From 33850c0ebd23ae615e6823993d441f46d80b1ff0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Apr 2010 13:12:57 +0200 Subject: [PATCH 0001/3747] Initial checkin of stuff that exists so far. --- .gitignore | 4 + examples/apishowcase/apishowcase.py | 30 ++ examples/apishowcase/static/style.css | 7 + examples/apishowcase/templates/counter.html | 12 + examples/apishowcase/templates/hello.html | 13 + examples/apishowcase/templates/index.html | 11 + examples/apishowcase/templates/layout.html | 8 + examples/minitwit/minitwit.py | 228 +++++++++++++ examples/minitwit/schema.sql | 21 ++ examples/minitwit/static/style.css | 178 ++++++++++ examples/minitwit/templates/layout.html | 32 ++ examples/minitwit/templates/login.html | 16 + examples/minitwit/templates/register.html | 19 ++ examples/minitwit/templates/timeline.html | 49 +++ flask.py | 356 ++++++++++++++++++++ 15 files changed, 984 insertions(+) create mode 100644 .gitignore create mode 100644 examples/apishowcase/apishowcase.py create mode 100644 examples/apishowcase/static/style.css create mode 100644 examples/apishowcase/templates/counter.html create mode 100644 examples/apishowcase/templates/hello.html create mode 100644 examples/apishowcase/templates/index.html create mode 100644 examples/apishowcase/templates/layout.html create mode 100644 examples/minitwit/minitwit.py create mode 100644 examples/minitwit/schema.sql create mode 100644 examples/minitwit/static/style.css create mode 100644 examples/minitwit/templates/layout.html create mode 100644 examples/minitwit/templates/login.html create mode 100644 examples/minitwit/templates/register.html create mode 100644 examples/minitwit/templates/timeline.html create mode 100644 flask.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2149b775 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +*.pyc +*.pyo +env diff --git a/examples/apishowcase/apishowcase.py b/examples/apishowcase/apishowcase.py new file mode 100644 index 00000000..b3ab2dd1 --- /dev/null +++ b/examples/apishowcase/apishowcase.py @@ -0,0 +1,30 @@ +from flask import Flask, abort, redirect, request, session, \ + render_template, url_for + +#: create a new flask applications. We pass it the name of our module +#: so that flask knows where to look for templates and static files. +app = Flask(__name__) + + +@app.route('/', methods=['GET']) +def index(): + """Show an overview page""" + return render_template('index.html') + + +@app.route('/hello/', methods=['GET', 'POST']) +def hello_user(): + """Ask the user for a name and redirect to :func:`hello`""" + if request.method == 'POST': + return redirect(url_for('hello', name=request.form['name'])) + return render_template('hello.html', name=None) + + +@app.route('/hello/', methods=['GET']) +def hello(name): + """Greet name friendly""" + return render_template('hello.html', name=name) + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/examples/apishowcase/static/style.css b/examples/apishowcase/static/style.css new file mode 100644 index 00000000..65a9ec14 --- /dev/null +++ b/examples/apishowcase/static/style.css @@ -0,0 +1,7 @@ +body { + font-family: 'Trebuchet MS', sans-serif; +} + +a { + color: #44AD80; +} diff --git a/examples/apishowcase/templates/counter.html b/examples/apishowcase/templates/counter.html new file mode 100644 index 00000000..ef888995 --- /dev/null +++ b/examples/apishowcase/templates/counter.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} +{% block body %} +

+ This is an example application that shows how + the Werkzeug powered Flask microframework works. +

+ The various parts of the example application: +

+{% endblock %} diff --git a/examples/apishowcase/templates/hello.html b/examples/apishowcase/templates/hello.html new file mode 100644 index 00000000..dc86737c --- /dev/null +++ b/examples/apishowcase/templates/hello.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block body %} + {% if name %} +

Hello {{ name }}!

+ {% else %} +

Hello Stranger …

+
+

… What's your name? +

+ +

+ {% endif %} +{% endblock %} diff --git a/examples/apishowcase/templates/index.html b/examples/apishowcase/templates/index.html new file mode 100644 index 00000000..d210d54f --- /dev/null +++ b/examples/apishowcase/templates/index.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} +{% block body %} +

+ This is an example application that shows how + the Werkzeug powered Flask microframework works. +

+ The various parts of the example application: +

+{% endblock %} diff --git a/examples/apishowcase/templates/layout.html b/examples/apishowcase/templates/layout.html new file mode 100644 index 00000000..166d39ec --- /dev/null +++ b/examples/apishowcase/templates/layout.html @@ -0,0 +1,8 @@ + +Flask API Showcase + +

Flask API Showcase

+{% if request.endpoint != 'index' %} + +{% endif %} +{% block body %}{% endblock %} diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py new file mode 100644 index 00000000..60f3fc0a --- /dev/null +++ b/examples/minitwit/minitwit.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +from __future__ import with_statement +import re +import time +import sqlite3 +from hashlib import md5 +from datetime import datetime +from contextlib import closing +from flask import Flask, request, session, url_for, redirect, \ + render_template, abort, g, flash, generate_password_hash, \ + check_password_hash + + +# configuration +DATABASE = '/tmp/minitwit.db' +PER_PAGE = 30 +DEBUG = True +SECRET_KEY = 'development key' + +# create our little application :) +app = Flask(__name__) + + +def connect_db(): + """Returns a new database connection to the database.""" + return sqlite3.connect(DATABASE) + + +def init_db(): + """Creates the database tables.""" + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + + +def query_db(query, args=(), one=False): + """Queries the database and returns a list of dictionaries.""" + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] + return (rv[0] if rv else None) if one else rv + + +def get_user_id(username): + """Convenience method to look up the id for a username""" + rv = g.db.execute('select user_id from user where username = ?', + [username]).fetchone() + return rv[0] if rv else None + + +def format_datetime(timestamp): + """Format a timestamp for display""" + return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') + + +def gravatar_url(email, size=80): + """Return the gravatar image for the given email address""" + return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ + (md5(email.lower().encode('utf-8')).hexdigest(), size) + + +@app.request_init +def before_request(): + """Make sure we are connected to the database each request and look + up the current user so that we know he's there. + """ + g.db = sqlite3.connect(DATABASE) + if 'user_id' in session: + g.user = query_db('select * from user where user_id = ?', + [session['user_id']], one=True) + + +@app.request_shutdown +def after_request(request): + """Closes the database again at the end of the request.""" + g.db.close() + return request + + +@app.route('/') +def timeline(): + if not 'user_id' in session: + return redirect(url_for('public_timeline')) + offset = request.args.get('offset', type=int) + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user + where message.author_id = user.user_id and ( + user.user_id = ? or + user.user_id in (select whom_id from follower + where who_id = ?)) + order by message.pub_date desc limit ?''', + [session['user_id'], session['user_id'], PER_PAGE])) + + +@app.route('/public') +def public_timeline(): + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user + where message.author_id = user.user_id + order by message.pub_date desc limit ?''', [PER_PAGE])) + + +@app.route('/') +def user_timeline(username): + profile_user = query_db('select * from user where username = ?', + [username], one=True) + if profile_user is None: + abort(404) + followd = False + if 'user_id' in session: + followed = query_db('''select 1 from follower where + follower.who_id = ? and follower.whom_id = ?''', + [session['user_id'], profile_user['user_id']], one=True) is not None + return render_template('timeline.html', messages=query_db(''' + select message.*, user.* from message, user where + user.user_id = message.author_id and user.user_id = ? + order by message.pub_date desc limit ?''', + [profile_user['user_id'], PER_PAGE]), followed=followed, + profile_user=profile_user) + + +@app.route('//follow') +def follow_user(username): + if not 'user_id' in session: + abort(401) + whom_id = get_user_id(username) + if whom_id is None: + abort(404) + g.db.execute('insert into follower (who_id, whom_id) values (?, ?)', + [session['user_id'], whom_id]) + g.db.commit() + flash('You are now following "%s"' % username) + return redirect(url_for('user_timeline', username=username)) + + +@app.route('//unfollow') +def unfollow_user(username): + if not 'user_id' in session: + abort(401) + whom_id = get_user_id(username) + if whom_id is None: + abort(404) + g.db.execute('delete from follower where who_id=? and whom_id=?', + [session['user_id'], whom_id]) + g.db.commit() + flash('You are no longer following "%s"' % username) + return redirect(url_for('user_timeline', username=username)) + + +@app.route('/add_message') +def add_message(): + if 'user_id' not in session: + abort(401) + if request.form['text']: + g.db.execute('''insert into message (author_id, text, pub_date) + values (?, ?, ?)''', (session['user_id'], request.form['text'], + int(time.time()))) + g.db.commit() + flash('Your message was recorded') + return redirect(url_for('timeline')) + + +@app.route('/login') +def login(): + if 'user_id' in session: + return redirect(url_for('timeline')) + error = None + if request.method == 'POST': + user = query_db('''select * from user where + username = ?''', [request.form['username']], one=True) + if user is None: + error = 'Invalid username' + elif not check_password_hash(user['pw_hash'], + request.form['password']): + error = 'Invalid password' + else: + flash('You were logged in') + session['user_id'] = user['user_id'] + return redirect(url_for('timeline')) + return render_template('login.html', error=error) + + +@app.route('/register') +def register(): + if 'user_id' in session: + return redirect(url_for('timeline')) + error = None + if request.method == 'POST': + if not request.form['username']: + error = 'You have to enter a username' + elif not request.form['email'] or \ + '@' not in request.form['email']: + error = 'You have to enter a valid email address' + elif not request.form['password']: + error = 'You have to enter a password' + elif request.form['password'] != request.form['password2']: + error = 'The two passwords to not match' + elif get_user_id(request.form['username']) is not None: + error = 'The username is already taken' + else: + g.db.execute('''insert into user ( + username, email, pw_hash) values (?, ?, ?)''', + [request.form['username'], request.form['email'], + generate_password_hash(request.form['password'])]) + g.db.commit() + flash('You were successfully registered and can login now') + return redirect(url_for('login')) + return render_template('register.html', error=error) + + +@app.route('/logout') +def logout(): + flash('You were logged out') + session.pop('user_id', None) + return redirect(url_for('public_timeline')) + + +# add some filters to jinja and set the secret key and debug mode +# from the configuration. +app.jinja_env.filters['datetimeformat'] = format_datetime +app.jinja_env.filters['gravatar'] = gravatar_url +app.secret_key = SECRET_KEY +app.debug = DEBUG + + +if __name__ == '__main__': + app.run() diff --git a/examples/minitwit/schema.sql b/examples/minitwit/schema.sql new file mode 100644 index 00000000..b64afbed --- /dev/null +++ b/examples/minitwit/schema.sql @@ -0,0 +1,21 @@ +drop table if exists user; +create table user ( + user_id integer primary key autoincrement, + username string not null, + email string not null, + pw_hash string not null +); + +drop table if exists follower; +create table follower ( + who_id integer, + whom_id integer +); + +drop table if exists message; +create table message ( + message_id integer primary key autoincrement, + author_id integer not null, + text string not null, + pub_date integer +); diff --git a/examples/minitwit/static/style.css b/examples/minitwit/static/style.css new file mode 100644 index 00000000..ebbed8c9 --- /dev/null +++ b/examples/minitwit/static/style.css @@ -0,0 +1,178 @@ +body { + background: #CAECE9; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; +} + +a { + color: #26776F; +} + +a:hover { + color: #333; +} + +input[type="text"], +input[type="password"] { + background: white; + border: 1px solid #BFE6E2; + padding: 2px; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + color: #105751; +} + +input[type="submit"] { + background: #105751; + border: 1px solid #073B36; + padding: 1px 3px; + font-family: 'Trebuchet MS', sans-serif; + font-size: 14px; + font-weight: bold; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + color: white; +} + +div.page { + background: white; + border: 1px solid #6ECCC4; + width: 700px; + margin: 30px auto; +} + +div.page h1 { + background: #6ECCC4; + margin: 0; + padding: 10px 14px; + color: white; + letter-spacing: 1px; + text-shadow: 0 0 3px #24776F; + font-weight: normal; +} + +div.page div.navigation { + background: #DEE9E8; + padding: 4px 10px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #eee; + color: #888; + font-size: 12px; + letter-spacing: 0.5px; +} + +div.page div.navigation a { + color: #444; + font-weight: bold; +} + +div.page h2 { + margin: 0 0 15px 0; + color: #105751; + text-shadow: 0 1px 2px #ccc; +} + +div.page div.body { + padding: 10px; +} + +div.page div.footer { + background: #eee; + color: #888; + padding: 5px 10px; + font-size: 12px; +} + +div.page div.followstatus { + border: 1px solid #ccc; + background: #E3EBEA; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 3px; + font-size: 13px; +} + +div.page ul.messages { + list-style: none; + margin: 0; + padding: 0; +} + +div.page ul.messages li { + margin: 10px 0; + padding: 5px; + background: #F0FAF9; + border: 1px solid #DBF3F1; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + min-height: 48px; +} + +div.page ul.messages p { + margin: 0; +} + +div.page ul.messages li img { + float: left; + padding: 0 10px 0 0; +} + +div.page ul.messages li small { + font-size: 0.9em; + color: #888; +} + +div.page div.twitbox { + margin: 10px 0; + padding: 5px; + background: #F0FAF9; + border: 1px solid #94E2DA; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +div.page div.twitbox h3 { + margin: 0; + font-size: 1em; + color: #2C7E76; +} + +div.page div.twitbox p { + margin: 0; +} + +div.page div.twitbox input[type="text"] { + width: 585px; +} + +div.page div.twitbox input[type="submit"] { + width: 70px; + margin-left: 5px; +} + +ul.flashes { + list-style: none; + margin: 10px 10px 0 10px; + padding: 0; +} + +ul.flashes li { + background: #B9F3ED; + border: 1px solid #81CEC6; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 4px; + font-size: 13px; +} + +div.error { + margin: 10px 0; + background: #FAE4E4; + border: 1px solid #DD6F6F; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + padding: 4px; + font-size: 13px; +} diff --git a/examples/minitwit/templates/layout.html b/examples/minitwit/templates/layout.html new file mode 100644 index 00000000..668e3895 --- /dev/null +++ b/examples/minitwit/templates/layout.html @@ -0,0 +1,32 @@ + +{% block title %}Welcome{% endblock %} | MiniTwit + +
+

MiniTwit

+ + {% with flashes = get_flashed_messages() %} + {% if flashes %} +
    + {% for message in flashes %} +
  • {{ message }} + {% endfor %} +
+ {% endif %} + {% endwith %} +
+ {% block body %}{% endblock %} +
+ +
diff --git a/examples/minitwit/templates/login.html b/examples/minitwit/templates/login.html new file mode 100644 index 00000000..ae776714 --- /dev/null +++ b/examples/minitwit/templates/login.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Sign In{% endblock %} +{% block body %} +

Sign In

+ {% if error %}
Error: {{ error }}
{% endif %} +
+
+
Username: +
+
Password: +
+
+
+
+{% endblock %} + diff --git a/examples/minitwit/templates/register.html b/examples/minitwit/templates/register.html new file mode 100644 index 00000000..ccb345d5 --- /dev/null +++ b/examples/minitwit/templates/register.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} +{% block title %}Sign Up{% endblock %} +{% block body %} +

Sign Up

+ {% if error %}
Error: {{ error }}
{% endif %} +
+
+
Username: +
+
E-Mail: +
+
Password: +
+
Password (repeat): +
+
+
+
+{% endblock %} diff --git a/examples/minitwit/templates/timeline.html b/examples/minitwit/templates/timeline.html new file mode 100644 index 00000000..892b8fcc --- /dev/null +++ b/examples/minitwit/templates/timeline.html @@ -0,0 +1,49 @@ +{% extends "layout.html" %} +{% block title %} + {% if request.endpoint == 'public_timeline' %} + Public Timeline + {% elif request.endpoint == 'user_timeline' %} + {{ profile_user.username }}'s Timeline + {% else %} + My Timeline + {% endif %} +{% endblock %} +{% block body %} +

{{ self.title() }}

+ {% if g.user %} + {% if request.endpoint == 'user_timeline' %} +
+ {% if g.user.user_id == profile_user.user_id %} + This is you! + {% elif followed %} + You are currently following this user. + Unfollow user. + {% else %} + You are not yet following this user. + . + {% endif %} +
+ {% elif request.endpoint == 'timeline' %} +
+

What's on your mind {{ g.user.username }}?

+
+

+

+
+ {% endif %} + {% endif %} +
    + {% for message in messages %} +
  • + {{ message.username }} + {{ message.text }} + — {{ message.pub_date|datetimeformat }} + {% else %} +

  • There are no messages so far. + {% endfor %} +
+{% endblock %} diff --git a/flask.py b/flask.py new file mode 100644 index 00000000..83d8a87e --- /dev/null +++ b/flask.py @@ -0,0 +1,356 @@ +# -*- coding: utf-8 -*- +""" + flask + ~~~~~ + + A microframework based on Werkzeug. It's extensively documented + and follows best practice patterns. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import pkg_resources +from threading import local +from jinja2 import Environment, PackageLoader +from werkzeug import Request, Response, LocalStack, LocalProxy +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.contrib.securecookie import SecureCookie + +# try to import the json helpers +try: + from simplejson import loads as load_json, dumps as dump_json +except ImportError: + try: + from json import loads as load_json, dumps as dump_json + except ImportError: + pass + +# utilities we import from Werkzeug and Jinja2 that are unused +# in the module but are exported as public interface. +from werkzeug import abort, redirect, secure_filename, cached_property, \ + html, import_string, generate_password_hash, check_password_hash +from jinja2 import Markup, escape + + +class FlaskRequest(Request): + """The request object used by default in flask. Remembers the + matched endpoint and view arguments. + """ + + def __init__(self, environ): + Request.__init__(self, environ) + self.endpoint = None + self.view_args = None + + +class FlaskResponse(Response): + """The response object that is used by default in flask. Works like the + response object from Werkzeug but is set to have a HTML mimetype by + default. + """ + default_mimetype = 'text/html' + + +class _RequestGlobals(object): + pass + + +class _RequestContext(object): + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + """ + + def __init__(self, app, environ): + self.app = app + self.url_adapter = app.url_map.bind_to_environ(environ) + self.request = app.request_class(environ) + self.session = app.open_session(self.request) + self.g = _RequestGlobals() + self.flashes = None + + +def url_for(endpoint, **values): + """Generates a URL to the given endpoint with the method provided. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + """ + return _request_ctx_stack.top.url_adapter.build(endpoint, values) + + +def jsonified(**values): + """Returns a json response""" + return current_app.response_class(dump_json(values), + mimetype='application/json') + + +def flash(message): + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + """ + session['_flashes'] = (session.get('_flashes', [])) + [message] + + +def get_flashed_messages(): + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = \ + session.pop('_flashes', []) + return flashes + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + """ + return current_app.jinja_env.get_template(template_name).render(context) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + """ + return current_app.jinja_env.from_string(source).render(context) + + +class Flask(object): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the application + and optionally a configuration. When it's created it sets up the + template engine and provides ways to register view functions. + """ + + #: the class that is used for request objects + request_class = FlaskRequest + + #: the class that is used for response objects + response_class = FlaskResponse + + #: path for the static files. If you don't want to use static files + #: you can set this value to `None` in which case no URL rule is added + #: and the development server will no longer serve any static files. + static_path = '/static' + + #: if a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + secret_key = None + + #: The secure cookie uses this for the name of the session cookie + session_cookie_name = 'session' + + #: options that are passed directly to the Jinja2 environment + jinja_options = dict( + autoescape=True, + extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] + ) + + def __init__(self, package_name): + self.debug = False + self.package_name = package_name + self.view_functions = {} + self.error_handlers = {} + self.request_init_funcs = [] + self.request_shutdown_funcs = [] + self.url_map = Map() + + if self.static_path is not None: + self.url_map.add(Rule(self.static_path + '/', + build_only=True, endpoint='static')) + + self.jinja_env = Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + self.jinja_env.globals.update( + url_for=url_for, + request=request, + session=session, + g=g, + get_flashed_messages=get_flashed_messages + ) + + def create_jinja_loader(self): + """Creates the Jinja loader. By default just a package loader for + the configured package is returned that looks up templates in the + `templates` folder. To add other loaders it's possible to + override this method. + """ + return PackageLoader(self.package_name) + + def run(self, host='localhost', port=5000, **options): + """Runs the application on a local development server""" + from werkzeug import run_simple + if 'debug' in options: + self.debug = options.pop('debug') + if self.static_path is not None: + options['static_files'] = { + self.static_path: (self.package_name, 'static') + } + options.setdefault('use_reloader', self.debug) + options.setdefault('use_debugger', self.debug) + return run_simple(host, port, self, **options) + + @cached_property + def test(self): + """A test client for this application""" + from werkzeug import Client + return Client(self, self.response_class, use_cookies=True) + + def open_resource(self, resource): + """Opens a resource from the application's resource folder""" + return pkg_resources.resource_stream(self.package_name, resource) + + def open_session(self, request): + """Creates or opens a new session. Default implementation requires + that `securecookie.secret_key` is set. + """ + key = self.secret_key + if key is not None: + return SecureCookie.load_cookie(request, self.session_cookie_name, + secret_key=key) + + def save_session(self, session, response): + """Saves the session if it needs updates.""" + if session is not None: + session.save_cookie(response, self.session_cookie_name) + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. Example:: + + @app.route('/') + def index(): + return 'Hello World' + """ + def decorator(f): + if 'endpoint' not in options: + options['endpoint'] = f.__name__ + self.url_map.add(Rule(rule, **options)) + self.view_functions[options['endpoint']] = f + return f + return decorator + + def errorhandler(self, code): + """A decorator that is used to register a function give a given + error code. Example:: + + @app.errorhandler(404) + def page_not_found(): + return 'This page does not exist', 404 + """ + def decorator(f): + self.error_handlers[code] = f + return f + return decorator + + def request_init(self, f): + """Registers a function to run before each request.""" + self.request_init_funcs.append(f) + return f + + def request_shutdown(self, f): + """Register a function to be run after each request.""" + self.request_shutdown_funcs.append(f) + return f + + def match_request(self): + """Matches the current request against the URL map and also + stores the endpoint and view arguments on the request object + is successful, otherwise the exception is stored. + """ + rv = _request_ctx_stack.top.url_adapter.match() + request.endpoint, request.view_args = rv + return rv + + def dispatch_request(self): + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + """ + try: + endpoint, values = self.match_request() + return self.view_functions[endpoint](**values) + except HTTPException, e: + handler = self.error_handlers.get(e.code) + if handler is None: + return e + return handler(e) + except Exception, e: + handler = self.error_handlers.get(500) + if self.debug or handler is None: + raise + return handler(e) + + def make_response(self, rv): + """Converts the return value from a view function to a real + response object that is an instance of :attr:`response_class`. + """ + if isinstance(rv, self.response_class): + return rv + if isinstance(rv, basestring): + return self.response_class(rv) + if isinstance(rv, tuple): + return self.response_class(*rv) + return self.response_class.force_type(rv, request.environ) + + def preprocess_request(self): + """Called before the actual request dispatching and will + call every as :func:`request_init` decorated function. + If any of these function returns a value it's handled as + if it was the return value from the view and further + request handling is stopped. + """ + for func in self.request_init_funcs: + rv = func() + if rv is not None: + return rv + + def process_response(self, response): + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. + """ + session = _request_ctx_stack.top.session + if session is not None: + self.save_session(session, response) + for handler in self.request_shutdown_funcs: + response = handler(response) + return response + + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + """ + _request_ctx_stack.push(_RequestContext(self, environ)) + try: + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + response = self.process_response(response) + return response(environ, start_response) + finally: + _request_ctx_stack.pop() + + def __call__(self, environ, start_response): + """Shortcut for :attr:`wsgi_app`""" + return self.wsgi_app(environ, start_response) + + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(lambda: _request_ctx_stack.top.app) +request = LocalProxy(lambda: _request_ctx_stack.top.request) +session = LocalProxy(lambda: _request_ctx_stack.top.session) +g = LocalProxy(lambda: _request_ctx_stack.top.g) From b15ad394279fc3b7f998fa56857f334a7c0156f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Apr 2010 13:23:18 +0200 Subject: [PATCH 0002/3747] Added setup.py and README --- README | 17 +++++++++++++++++ setup.py | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 README create mode 100644 setup.py diff --git a/README b/README new file mode 100644 index 00000000..9b82ae29 --- /dev/null +++ b/README @@ -0,0 +1,17 @@ + + // Flask // + + because a pocket knife is not the only thing that + might come in handy + + + ~ What is Flask? + + Flask is a microframework for Python based on Werkzeug + and Jinja2. It's intended for small scale applications + and was development with best intentions in mind. + + ~ Is it ready? + + Nope, this is still work in progress, but I am happy to + accept patches and improvements already. diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..72ef584e --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup + + +setup( + name='Flask', + version='0.1', + url='http://github.com/mitsuhiko/flask/', + license='BSD', + author='Armin Ronacher', + author_email='armin.ronacher@active-4.com', + description='A microframework based on Werkzeug, Jinja2 and good intentions', + modules=['flask'], + zip_safe=False, + platforms='any', + install_requires=[ # yes, as of now we need the development versions + 'Werkzeug==dev', + 'Jinja2==dev', + ] +) From 4ec7d2a0d8eac4f915dc0d38a886cd57045bb0c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Apr 2010 16:02:14 +0200 Subject: [PATCH 0003/3747] Started working on documentation. So far just the stable parts :) --- .gitignore | 1 + docs/.gitignore | 1 + docs/Makefile | 118 +++++++++++++++++ docs/api.rst | 15 +++ docs/conf.py | 236 ++++++++++++++++++++++++++++++++++ docs/index.rst | 11 ++ docs/make.bat | 139 ++++++++++++++++++++ examples/minitwit/minitwit.py | 2 +- flask.py | 127 ++++++++++++++++-- 9 files changed, 641 insertions(+), 9 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/Makefile create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat diff --git a/.gitignore b/.gitignore index 2149b775..0edef0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.pyc *.pyo env +*.egg-info diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..e35d8850 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..52d78d9e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,118 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp epub latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) _build/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask" + @echo "# ln -s _build/devhelp $$HOME/.local/share/devhelp/Flask" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +latexpdf: latex + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo "Running LaTeX files through pdflatex..." + make -C _build/latex all-pdf + @echo "pdflatex finished; the PDF files are in _build/latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..19bd68b0 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,15 @@ +API +=== + +.. module:: flask + +This part of the documentation covers all the interfaces of Flask. For +parts where Flask depends on external libraries, we document the most +important right here and provide links to the canonical documentation. + + +General Structure +----------------- + +.. autoclass:: Flask + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..1e4fb807 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# +# Flask documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 6 15:24:58 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Flask' +copyright = u'2010, Armin Ronacher' + +import pkg_resources + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +release = __import__('pkg_resources').get_distribution('Flask').version +version = '.'.join(release.split('.')[:2]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Flaskdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'Flask.tex', u'Flask Documentation', + u'Armin Ronacher', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +#epub_title = '' +#epub_author = '' +#epub_publisher = '' +#epub_copyright = '' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..217ced04 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,11 @@ +Welcome to Flask +================ + +Welcome to Flask's documentation. + +Contents: + +.. toctree:: + :maxdepth: 2 + + api diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..3ad12879 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,139 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% _build/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 60f3fc0a..264325ec 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -57,7 +57,7 @@ def format_datetime(timestamp): def gravatar_url(email, size=80): """Return the gravatar image for the given email address""" return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ - (md5(email.lower().encode('utf-8')).hexdigest(), size) + (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) @app.request_init diff --git a/flask.py b/flask.py index 83d8a87e..20306a81 100644 --- a/flask.py +++ b/flask.py @@ -125,9 +125,22 @@ def render_template_string(source, **context): class Flask(object): """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the application - and optionally a configuration. When it's created it sets up the - template engine and provides ways to register view functions. + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an `__init__.py` file inside) or a standard module (just a `.py` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the `__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) """ #: the class that is used for request objects @@ -156,11 +169,43 @@ class Flask(object): ) def __init__(self, package_name): + #: the debug flag. Set this to `True` to enable debugging of + #: the application. In debug mode the debugger will kick in + #: when an unhandled exception ocurrs and the integrated server + #: will automatically reload the application if changes in the + #: code are detected. self.debug = False + + #: the name of the package or module. Do not change this once + #: it was set by the constructor. self.package_name = package_name + + #: a dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: to register a view function, use the :meth:`route` decorator. self.view_functions = {} + + #: a dictionary of all registered error handlers. The key is + #: be the error code as integer, the value the function that + #: should handle that error. + #: To register a error handler, use the :meth:`errorhandler` + #: decorator. self.error_handlers = {} + + #: a list of functions that should be called at the beginning + #: of the request before request dispatching kicks in. This + #: can for example be used to open database connections or + #: getting hold of the currently logged in user. + #: To register a function here, use the :meth:`request_init` + #: decorator. self.request_init_funcs = [] + + #: a list of functions that are called at the end of the + #: request. Tha function is passed the current response + #: object and modify it in place or replace it. + #: To register a function here use the :meth:`request_shtdown` + #: decorator. self.request_shutdown_funcs = [] self.url_map = Map() @@ -168,6 +213,9 @@ class Flask(object): self.url_map.add(Rule(self.static_path + '/', build_only=True, endpoint='static')) + #: the Jinja2 environment. It is created from the + #: :attr:`jinja_options` and the loader that is returned + #: by the :meth:`create_jinja_loader` function. self.jinja_env = Environment(loader=self.create_jinja_loader(), **self.jinja_options) self.jinja_env.globals.update( @@ -187,7 +235,10 @@ class Flask(object): return PackageLoader(self.package_name) def run(self, host='localhost', port=5000, **options): - """Runs the application on a local development server""" + """Runs the application on a local development server. If the + :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + """ from werkzeug import run_simple if 'debug' in options: self.debug = options.pop('debug') @@ -206,12 +257,30 @@ class Flask(object): return Client(self, self.response_class, use_cookies=True) def open_resource(self, resource): - """Opens a resource from the application's resource folder""" + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schemal.sql + /static + /style.css + /template + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + """ return pkg_resources.resource_stream(self.package_name, resource) def open_session(self, request): - """Creates or opens a new session. Default implementation requires - that `securecookie.secret_key` is set. + """Creates or opens a new session. Default implementation stores all + session data in a signed cookie. This requires that the + :attr:`secret_key` is set. """ key = self.secret_key if key is not None: @@ -219,7 +288,9 @@ class Flask(object): secret_key=key) def save_session(self, session, response): - """Saves the session if it needs updates.""" + """Saves the session if it needs updates. For the default + implementation, check :meth:`open_session`. + """ if session is not None: session.save_cookie(response, self.session_cookie_name) @@ -230,6 +301,46 @@ class Flask(object): @app.route('/') def index(): return 'Hello World' + + Variables parts in the route can be specified with angular + brackets (``/user/``). By default a variable part + in the URL accepts any string without a slash however a differnt + converter can be specified as well by using ````. + + Variable parts are passed to the view function as keyword + arguments. + + The following converters are possible: + + =========== =========================================== + `int` accepts integers + `float` like `int` but for floating point values + `path` like the default but also accepts slashes + =========== =========================================== + + Here some examples:: + + @app.route('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + pass + + The :meth:`route` decorator accepts a couple of other arguments + as well: + + :param methods: a list of methods this rule should be limited + to (``GET``, ``POST`` etc.) + :param subdomain: specifies the rule for the subdoain in case + subdomain matching is in use. + :param strict_slashes: can be used to disable the strict slashes + setting for this rule. See above. """ def decorator(f): if 'endpoint' not in options: From 44b42e0fbd93d86e0f4e929bda8e5fb63e81035d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Apr 2010 16:16:34 +0200 Subject: [PATCH 0004/3747] Added a README file for MiniTwit. --- examples/minitwit/README | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 examples/minitwit/README diff --git a/examples/minitwit/README b/examples/minitwit/README new file mode 100644 index 00000000..f054fd8f --- /dev/null +++ b/examples/minitwit/README @@ -0,0 +1,21 @@ + + / MiniTwit / + + because writing todo lists is not fun + + + ~ What is MiniTwit? + + A SQLite and Flask powered twitter clone + + ~ How do I use it? + + 1. edit the configurtion in the minitwit.py file + + 2. fire up a python shell and run this: + + >>> from minitwit import init_db; init_db() + + 3. now you can run the minitwit.py file with your + python interpreter and the application will + greet you on http://localhost:5000/ From 3b36bef2e6165bb4dad73d17f23ee1879e99f497 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 8 Apr 2010 19:03:15 +0200 Subject: [PATCH 0005/3747] Improved documentation, added a contextmanager for request binding --- docs/api.rst | 154 +++++++++++++++++++++++++++++++++- docs/conf.py | 7 +- examples/minitwit/minitwit.py | 4 +- flask.py | 61 +++++++++----- 4 files changed, 198 insertions(+), 28 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 19bd68b0..5ab86e7d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,8 +8,158 @@ parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. -General Structure ------------------ +Application Object +------------------ .. autoclass:: Flask :members: + +Incoming Request Data +--------------------- + +.. class:: request + + To access incoming request data, you can use the global `request` + object. Flask parses incoming request data for you and gives you + access to it through that global object. Internally Flask makes + sure that you always get the correct data for the active thread if you + are in a multithreaded environment. + + The request object is an instance of a :class:`~werkzeug.Request` + subclass and provides all of the attributes Werkzeug defines. This + just shows a quick overview of the most important ones. + + .. attribute:: form + + A :class:`~werkzeug.MultiDict` with the parsed form data from `POST` + or `PUT` requests. Please keep in mind that file uploads will not + end up here, but instead in the :attr:`files` attribute. + + .. attribute:: args + + A :class:`~werkzeug.MultiDict` with the parsed contents of the query + string. (The part in the URL after the question mark). + + .. attribute:: values + + A :class:`~werkzeug.CombinedMultiDict` with the contents of both + :attr:`form` and :attr:`args`. + + .. attribute:: cookies + + A :class:`dict` with the contents of all cookies transmitted with + the request. + + .. attribute:: stream + + If the incoming form data was not encoded with a known encoding (for + example it was transmitted as JSON) the data is stored unmodified in + this stream for consumption. For example to read the incoming + request data as JSON, one can do the following:: + + json_body = simplejson.load(request.stream) + + .. attribute:: files + + A :class:`~werkzeug.MultiDict` with files uploaded as part of a + `POST` or `PUT` request. Each file is stored as + :class:`~werkzeug.FileStorage` object. It basically behaves like a + standard file object you know from Python, with the difference that + it also has a :meth:`~werkzeug.FileStorage.save` function that can + store the file on the filesystem. + + .. attribute:: method + + The current request method (``POST``, ``GET`` etc.) + + .. attribute:: path + .. attribute:: script_root + .. attribute:: url + .. attribute:: base_url + .. attribute:: url_root + + Provides different ways to look at the current URL. Imagine your + application is listening on the following URL:: + + http://www.example.com/myapplication + + And a user requests the following URL:: + + http://www.example.com/myapplication/page.html?x=y + + In this case the values of the above mentioned attributes would be + the following: + + ============= ====================================================== + `path` ``/page.html`` + `script_root` ``/myapplication`` + `url` ``http://www.example.com/myapplication/page.html`` + `base_url` ``http://www.example.com/myapplication/page.html?x=y`` + `root_url` ``http://www.example.com/myapplication/`` + ============= ====================================================== + + +Sessions +-------- + +If you have the :attr:`Flask.secret_key` set you can use sessions in Flask +applications. A session basically makes it possible to remember +information from one request to another. The way Flask does this is by +using a signed cookie. So the user can look at the session contents, but +not modify it unless he knows the secret key, so make sure to set that to +something complex and unguessable. + +To access the current session you can use the :class:`session` object: + +.. class:: session + + The session object works pretty much like an ordinary dict, with the + difference that it keeps track on modifications. + + The following attributes are interesting: + + .. attribute:: new + + `True` if the session is new, `False` otherwise. + + .. attribute:: modified + + `True` if the session object detected a modification. Be advised + that modifications on mutable structures are not picked up + automatically, in that situation you have to explicitly set the + attribute to `True` yourself. Here an example:: + + # this change is not picked up because a mutable object (here + # a list) is changed. + session['objects'].append(42) + # so mark it as modified yourself + session.modified = True + + +Useful Functions and Classes +---------------------------- + +.. autofunction:: url_for + +.. autofunction:: abort + +.. autofunction:: redirect + +.. autofunction:: escape + +.. autoclass:: Markup + :members: escape, unescape, striptags + +Message Flashing +---------------- + +.. autofunction:: flash + +.. autofunction:: get_flashed_messages + +Template Rendering +------------------ + +.. autofunction:: render_template + +.. autofunction:: render_template_string diff --git a/docs/conf.py b/docs/conf.py index 1e4fb807..85c52700 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -234,3 +234,8 @@ latex_documents = [ # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 + +intersphinx_mapping = { + 'http://docs.python.org/dev': None, + 'http://werkzeug.pocoo.org/documentation/dev/': None +} diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 264325ec..fccf969c 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -7,8 +7,8 @@ from hashlib import md5 from datetime import datetime from contextlib import closing from flask import Flask, request, session, url_for, redirect, \ - render_template, abort, g, flash, generate_password_hash, \ - check_password_hash + render_template, abort, g, flash +from werkzeug import check_password_hash, generate_password_hash # configuration diff --git a/flask.py b/flask.py index 20306a81..794f234a 100644 --- a/flask.py +++ b/flask.py @@ -13,25 +13,17 @@ import os import sys import pkg_resources from threading import local +from contextlib import contextmanager from jinja2 import Environment, PackageLoader -from werkzeug import Request, Response, LocalStack, LocalProxy +from werkzeug import Request, Response, LocalStack, LocalProxy, \ + create_environ, cached_property from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie -# try to import the json helpers -try: - from simplejson import loads as load_json, dumps as dump_json -except ImportError: - try: - from json import loads as load_json, dumps as dump_json - except ImportError: - pass - # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. -from werkzeug import abort, redirect, secure_filename, cached_property, \ - html, import_string, generate_password_hash, check_password_hash +from werkzeug import abort, redirect from jinja2 import Markup, escape @@ -83,12 +75,6 @@ def url_for(endpoint, **values): return _request_ctx_stack.top.url_adapter.build(endpoint, values) -def jsonified(**values): - """Returns a json response""" - return current_app.response_class(dump_json(values), - mimetype='application/json') - - def flash(message): """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, @@ -113,6 +99,7 @@ def render_template(template_name, **context): """Renders a template from the template folder with the given context. """ + current_app.update_template_context(context) return current_app.jinja_env.get_template(template_name).render(context) @@ -120,6 +107,7 @@ def render_template_string(source, **context): """Renders a template from the given template source string with the given context. """ + current_app.update_template_context(context) return current_app.jinja_env.from_string(source).render(context) @@ -220,9 +208,6 @@ class Flask(object): **self.jinja_options) self.jinja_env.globals.update( url_for=url_for, - request=request, - session=session, - g=g, get_flashed_messages=get_flashed_messages ) @@ -234,6 +219,15 @@ class Flask(object): """ return PackageLoader(self.package_name) + def update_template_context(self, context): + """Update the template context with some commonly used variables. + This injects request, session and g into the template context. + """ + reqctx = _request_ctx_stack.top + context['request'] = reqctx.request + context['session'] = reqctx.session + context['g'] = reqctx.g + def run(self, host='localhost', port=5000, **options): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload @@ -443,17 +437,38 @@ class Flask(object): app.wsgi_app = MyMiddleware(app.wsgi_app) """ - _request_ctx_stack.push(_RequestContext(self, environ)) - try: + with self.request_context(environ): rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() response = self.make_response(rv) response = self.process_response(response) return response(environ, start_response) + + @contextmanager + def request_context(self, environ): + """Creates a request context from the given environment and binds + it to the current context. This must be used in combination with + the `with` statement because the request is only bound to the + current context for the duration of the `with` block. + + Example usage:: + + with app.request_context(environ): + do_something_with(request) + """ + _request_ctx_stack.push(_RequestContext(self, environ)) + try: + yield finally: _request_ctx_stack.pop() + def test_request_context(self, *args, **kwargs): + """Creates a WSGI environment from the given values (see + :func:`werkzeug.create_environ` for more information). + """ + return self.request_context(create_environ(*args, **kwargs)) + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`""" return self.wsgi_app(environ, start_response) From 4aa76212d1be9b17a57249549c2a73fb10b6cda8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 01:32:39 +0200 Subject: [PATCH 0006/3747] Updated documentation. Starting to look pretty good --- docs/api.rst | 20 +++ docs/index.rst | 2 + docs/installation.rst | 6 + docs/quickstart.rst | 312 ++++++++++++++++++++++++++++++++++ examples/minitwit/minitwit.py | 6 +- flask.py | 17 +- 6 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 docs/installation.rst create mode 100644 docs/quickstart.rst diff --git a/docs/api.rst b/docs/api.rst index 5ab86e7d..6c763393 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -68,6 +68,10 @@ Incoming Request Data it also has a :meth:`~werkzeug.FileStorage.save` function that can store the file on the filesystem. + .. attribute:: environ + + The underlying WSGI environment. + .. attribute:: method The current request method (``POST``, ``GET`` etc.) @@ -136,6 +140,22 @@ To access the current session you can use the :class:`session` object: session.modified = True +Application Globals +------------------- + +To share data that is valid for one request only from one function to +another, a global variable is not good enough because it would break in +threaded environments. Flask provides you with a special object that +ensures it is only valid for the active request and that will return +different values for each request. In a nutshell: it does the right +thing, like it does for :class:`request` and :class:`session`. + +.. data:: g + + Just store on this whatever you want. For example a database + connection or the user that is currently logged in. + + Useful Functions and Classes ---------------------------- diff --git a/docs/index.rst b/docs/index.rst index 217ced04..5e5d8622 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,4 +8,6 @@ Contents: .. toctree:: :maxdepth: 2 + installation + quickstart api diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 00000000..8b0804d2 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,6 @@ +.. _installation: + +Installation +============ + +Blafasel, add me diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..0bfd7df7 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,312 @@ +Quickstart +========== + +Eager to get started? This page gives a good introduction in how to gets +started with Flask. This assumes you already have Flask installed. If +you do not, head over to the :ref:`installation` section. + + +A Minimal Application +--------------------- + +A minimal Flask application looks something like that:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def hello_world(): + return "Hello World!" + + if __name__ == '__main__': + app.run() + +If you now start that application with your Python interpreter and head +over to `http://localhost:5000/ `_, you should see +your hello world application. + +So what did that code do? + +1. first we imported the :class:`~flask.Flask` class. An instance of this + class will be our WSGI application. +2. next we create an instance of it. We pass it the name of the module / + package. This is needed so that Flask knows where it should look for + templates, static files and so on. +3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask + what URL should trigger our function. +4. The function then has a name which is also used to generate URLs to + that particular function, and returns the message we want to display in + the user's browser. +5. Finally we use the :meth:`~flask.Flask.run` function to run the + local server with our application. The ``if __name__ == '__main__':`` + makes sure the server only runs if the script is executed directly from + the Python interpreter and not used as imported module. + + +Routing +------- + +As you have seen above, the :meth:`~flask.Flask.route` decorator is used +to bind a function to a URL. But there is more to it! You can make +certain parts of the URL dynamic and attach multiple rules to a function. + +Here some examples:: + + @app.route('/') + def index(): + return 'Index Page' + + @app.route('/hello') + def hello(): + return 'Hello World' + + +Variable Rules +`````````````` + +Modern web applications have beautiful URLs. This helps people remember +the URLs which is especially handy for applications that are used from +mobile devices with slower network connections. If the user can directly +go to the desired page without having to hit the index page it is more +likely he will like the page and come back next time. + +To add variable parts to a URL you can mark these special sections as +````. Such a part is then passed as keyword argument to +your function. Optionally a converter can be specifed by specifying a +rule with ````. Here some nice examples:: + + @app.route('/user/') + def show_user_profile(username): + # show the user profile for that user + pass + + @app.route('/post/') + def show_post(post_id): + # show the post with the given id, the id is an integer + pass + +The following converters exist: + +=========== =========================================== +`int` accepts integers +`float` like `int` but for floating point values +`path` like the default but also accepts slashes +=========== =========================================== + + +HTTP Methods +```````````` + +HTTP knows different methods to access URLs. By default a route only +answers to ``GET`` requests, but that can be changed by providing the +`methods` argument to the :meth:`~flask.Flask.route` decorator. Here some +examples:: + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + do_the_login() + else: + show_the_login_form() + +If ``GET`` is present, ``HEAD`` will be added automatically for you. You +don't have to deal with that. It will also make sure that ``HEAD`` +requests are handled like the RFC demands, so you can completely ignore +that part of the HTTP specification. + + +Accessing Request Data +---------------------- + +For web applications it's crucial to react to the data a client sent to +the server. In Flask this information is provided by the global +:class:`~flask.request` object. If you have some experience with Python +you might be wondering how that object can be global and how Flask +manages to still be threadsafe. The answer are context locals: + +Context Locals +`````````````` + +.. admonition:: Insider Information + + If you want to understand how that works and how you can implement + tests with context locals, read this section, otherwise just skip it. + +Certain objects in Flask are global objects, but not just a standard +global object, but actually a proxy to an object that is local to a +specific context. What a mouthful. But that is actually quite easy to +understand. + +Imagine the context being the handling thread. A request comes in and the +webserver decides to spawn a new thread (or something else, the +underlying object is capable of dealing with other concurrency systems +than threads as well). When Flask starts its internal request handling it +figures out that the current thread is the active context and binds the +current application and the WSGI environments to that context (thread). +It does that in an intelligent way that one application can invoke another +application without breaking. + +So what does this mean to you? Basically you can completely ignore that +this is the case unless you are unittesting or something different. You +will notice that code that depends on a request object will suddenly break +because there is no request object. The solution is creating a request +object yourself and binding it to the context. The easiest solution for +unittesting is by using the :meth:`~flask.Flask.test_request_context` +context manager. In combination with the `with` statement it will bind a +test request so that you can interact with it. Here an example:: + + from flask import request + + with app.test_request_context('/hello', method='POST'): + # now you can do something with the request until the + # end of the with block, such as basic assertions: + assert request.path == '/hello' + assert request.method == 'POST' + +The other possibility is passing a whole WSGI environment to the +:meth:`~flask.Flask.request_context` method:: + + from flask import request + + with app.request_context(environ): + assert request.method == 'POST' + +The Request Object +`````````````````` + +The request object is documented in the API section and we will not cover +it here in detail (see :class:`~flask.request`), but just mention some of +the most common operations. First of all you have to import it from the +the `flask` module:: + + from flask import request + +The current request method is available by using the +:attr:`~flask.request.method` attribute. To access form data (data +transmitted in a `POST` or `PUT` request) you can use the +:attr:`~flask.request.form` attribute. Here a full example of the two +attributes mentioned above:: + + @app.route('/login', method=['POST', 'GET']) + def login(): + error = None + if request.method == 'POST': + if valid_login(request.form['username'], + request.form['password']): + return log_the_user_in(request.form['username']) + else: + error = 'Invalid username/password' + # this is executed if the request method was GET or the + # credentials were invalid + +What happens if the key does not exist in the `form` attribute? In that +case a special :exc:`KeyError` is raised. You can catch it like a +standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request +error page is shown instead. So for many situations you don't have to +deal with that problem. + +To access parameters submitted in the URL (``?key=value``) you can use the +:attr:`~flask.request.args` attribute:: + + searchword = request.args.get('q', '') + +We recommend accessing URL parameters with `get` or by catching the +`KeyError` because users might change the URL and presenting them a 400 +bad request page in that case is a bit user unfriendly. + +For a full list of methods and attribtues on that object, head over to the +:class:`~flask.request` documentation. + + +File Uploads +```````````` + +Obviously you can handle uploaded files with Flask just as easy. Just +make sure not to forget to set the ``enctype="multipart/form-data"`` +attribtue on your HTML form, otherwise the browser will not transmit your +files at all. + +Uploaded files are stored in memory or at a temporary location on the +filesystem. You can access those files by looking at the +:attr:`~flask.request.files` attribute on the request object. Each +uploaded file is stored in that dictionary. It behaves just like a +standard Python :class:`file` object, but it also has a +:meth:`~werkzeug.FileStorage.save` method that allows you to store that +file on the filesystem of the server. Here a simple example how that +works:: + + from flask import request + + @app.route('/upload', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + f = request.files['the_file'] + f.save('/var/www/uploads/uploaded_file.txt') + ... + +If you want to know how the file was named on the client before it was +uploaded to your application, you can access the +:attr:`~werkzeug.FileStorage.filename` attribute. However please keep in +mind that this value can be forged so never ever trust that value. If you +want to use the filename of the client to store the file on the server, +pass it through the :func:`~werkzeug.secure_filename` function that +Werkzeug provides for you:: + + from flask import request + from werkzeug import secure_filename + + @app.route('/upload', methods=['GET', 'POST']) + def upload_file(): + if request.method == 'POST': + f= request.files['the_file'] + f.save('/var/www/uploads/' + secure_filename(f.filename)) + ... + +Cookies +``````` + +To access cookies you can use the :attr:`~flask.request.cookies` +attribute. Again this is a dictionary with all the cookies the client +transmits. If you want to use sessions, do not use the cookies directly +but instead use the :ref:`sessions` in Flask that add some security on top +of cookies for you. + +.. _sessions: + +Sessions +-------- + +Besides the request object there is also a second object called +:class:`~flask.session` that allows you to store information specific to a +user from one request to the next. This is implemented on top of cookies +for you and signes the cookies cryptographically. What this means is that +the user could look at the contents of your cookie but not modify it, +unless he knows the secret key used for signing. + +In order to use sessions you have to set a secret key. Here is how +sessions work:: + + from flask import session, redirect, url_for, escape + + @app.route('/') + def index(): + if 'username' in session: + return 'Logged in as %s' % escape(session['username']) + return 'You are not logged in' + + @app.route('/login', methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + session['username'] = request.form['username'] + return redirect(url_for('index')) + return ''' +
+

+

+

+ ''' + + @app.route('/logout') + def logout(): + # remove the username from the session if its there + session.pop('username', None) diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index fccf969c..ebb304a7 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -148,7 +148,7 @@ def unfollow_user(username): return redirect(url_for('user_timeline', username=username)) -@app.route('/add_message') +@app.route('/add_message', methods=['POST']) def add_message(): if 'user_id' not in session: abort(401) @@ -161,7 +161,7 @@ def add_message(): return redirect(url_for('timeline')) -@app.route('/login') +@app.route('/login', methods=['GET', 'POST']) def login(): if 'user_id' in session: return redirect(url_for('timeline')) @@ -181,7 +181,7 @@ def login(): return render_template('login.html', error=error) -@app.route('/register') +@app.route('/register', methods=['GET', 'POST']) def register(): if 'user_id' in session: return redirect(url_for('timeline')) diff --git a/flask.py b/flask.py index 794f234a..9ea9db5c 100644 --- a/flask.py +++ b/flask.py @@ -326,11 +326,25 @@ class Flask(object): def show_post(post_id): pass + An important detail to keep in mind is how Flask deals with trailing + slashes. The idea is to keep each URL unique so the following rules + apply: + + 1. If a rule ends with a slash and is requested without a slash + by the user, the user is automatically redirected to the same + page with a trailing slash attached. + 2. If a rule does not end with a trailing slash and the user request + the page with a trailing slash, a 404 not found is raised. + + This is consistent with how web servers deal with static files. This + also makes it possible to use relative link targets safely. + The :meth:`route` decorator accepts a couple of other arguments as well: :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.) + to (``GET``, ``POST`` etc.). By default a rule + just listens for ``GET`` (and implicitly ``HEAD``). :param subdomain: specifies the rule for the subdoain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes @@ -339,6 +353,7 @@ class Flask(object): def decorator(f): if 'endpoint' not in options: options['endpoint'] = f.__name__ + options.setdefault('methods', ('GET',)) self.url_map.add(Rule(rule, **options)) self.view_functions[options['endpoint']] = f return f From 625eba4bb403296a8bbc1e72cc9cf4a5126f6c02 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 12:49:01 +0200 Subject: [PATCH 0007/3747] Heavily improved quickstart section. --- docs/quickstart.rst | 224 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0bfd7df7..e95e467a 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -93,6 +93,40 @@ The following converters exist: `path` like the default but also accepts slashes =========== =========================================== +URL Building +```````````` + +If it can match URLs, can it also generate them? Of course you can. To +build a URL to a specific function you can use the :func:`~flask.url_for` +function. It accepts the name of the function as first argument and a +number of keyword arguments, each corresponding to the variable part of +the URL rule. Here some examples: + +>>> from flask import Flask, url_for +>>> app = Flask(__name__) +>>> @app.route('/') +... def index(): pass +... +>>> @app.route('/login') +... def login(): pass +... +>>> @app.route('/user/') +... def profile(username): pass +... +>>> with app.test_request_context(): +... print url_for('index') +... print url_for('login') +... print url_for('profile', username='John Doe') +... +/ +/login +/user/John%20Doe + +(This also uses the :meth:`~flask.Flask.test_request_context` method +explained below. It basically tells flask to think we are handling a +request even though we are not, we are in an interactive Python shell. +Have a look at the explanation below. :ref:`context-locals`). + HTTP Methods ```````````` @@ -114,6 +148,80 @@ don't have to deal with that. It will also make sure that ``HEAD`` requests are handled like the RFC demands, so you can completely ignore that part of the HTTP specification. +Rendering Templates +------------------- + +Generating HTML from within Python is not fun, and actually pretty +cumbersome because you have to do the HTML escaping on your own to keep +the application secure. Because of that Flask configures the `Jinja2 +`_ template engine for you automatically. + +To render a template you can use the :func:`~flask.render_template` +method. All you have to do is to provide the name of the template and the +variables you want to pass to the template engine as keyword arguments. +Here a simple example of how to render a template:: + + from flask import render_template + + @app.route('/hello/') + @app.route('/hello/') + def hello(name=None): + return render_template('hello.html', name=name) + +Flask will look for templates in the `templates` folder. So if your +application is a module, that folder is next to that module, if it's a +pacakge it's actually inside your package: + +**Case 1**: a module:: + + /application.py + /templates + /hello.html + +**Case 2**: a package:: + + /application + /__init__.py + /templates + /hello.html + +For templates you can use the full power of Jinja2 templates. Head over +to the `Jinja2 Template Documentation +`_ for more information. + +Here an example template: + +.. sourcecode:: html+jinja + + + Hello from Flask + {% if name %} +

Hello {{ name }}!

+ {% else %} +

Hello World!

+ {% endif %} + +Inside templates you also have access to the :class:`~flask.request`, +:class:`~flask.session` and :class:`~flask.g` objects as well as the +:func:`~flask.get_flashed_messages` function. + +Automatic escaping is enabled, so if name contains HTML it will be escaped +automatically. If you can trust a variable and you know that it will be +safe HTML (because for example it came from a module that converts wiki +markup to HTML) you can mark it as safe by using the +:class:`~jinja2.Markup` class or by using the ``|safe`` filter in the +template. Head over to the Jinja 2 documentation for more examples. + +Here a basic introduction in how the :class:`~jinja2.Markup` class works: + +>>> from flask import Markup +>>> Markup('Hello %s!') % 'hacker' +Markup(u'Hello <blink>hacker</blink>!') +>>> Markup.escape('hacker') +Markup(u'<blink>hacker</blink>') +>>> Markup('Marked up » HTML').striptags() +u'Marked up \xbb HTML' + Accessing Request Data ---------------------- @@ -124,6 +232,9 @@ the server. In Flask this information is provided by the global you might be wondering how that object can be global and how Flask manages to still be threadsafe. The answer are context locals: + +.. _context-locals: + Context Locals `````````````` @@ -271,6 +382,39 @@ transmits. If you want to use sessions, do not use the cookies directly but instead use the :ref:`sessions` in Flask that add some security on top of cookies for you. + +Redirects and Errors +-------------------- + +To redirect a user to somewhere else you can use the +:func:`~flask.redirect` function, to abort a request early with an error +code the :func:`~flask.abort` function. Here an example how this works:: + + from flask import abort, redirect, url_for + + @app.route('/') + def index(): + return redirect(url_for('login')) + + @app.route('/login') + def login(): + abort(401) + this_is_never_executed() + +This is a rather pointless example because a user will be redirected from +the index to a page he cannot access (401 means access denied) but it +shows how that works. + +By default a black and white error page is shown for each error code. If +you want to customize the error page, you can use the +:meth:`~flask.Flask.errorhandler` decorator:: + + from flask import render_template + + @app.errorhandler(404) + def page_not_found(error): + return render_template('page_not_found.html') + .. _sessions: Sessions @@ -310,3 +454,83 @@ sessions work:: def logout(): # remove the username from the session if its there session.pop('username', None) + +Message Flashing +---------------- + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback he will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it next +request and only next request. This is usually combined with a layout +template that does this. + +So here a full example:: + + from flask import flash, redirect, url_for, render_template + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or \ + request.form['password'] != 'secret': + error = 'Invalid credentials' + else: + flash('You were sucessfully logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) + +And here the ``layout.html`` template which does the magic: + +.. sourcecode:: html+jinja + + + My Application + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block body %}{% endblock %} + +And here the index.html template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Overview

+

Do you want to log in? + {% endblock %} + +And of course the login template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Login

+ {% if error %} +

Error: {{ error }} + {% endif %} +

+
+
Username: +
+
Password: +
+
+

+

+ {% endblock %} From 4edec48b74dbb9d571e927b69b24c30ec0624975 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 13:13:39 +0200 Subject: [PATCH 0008/3747] Added installation docs --- docs/installation.rst | 99 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 8b0804d2..b8375cd7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,4 +3,101 @@ Installation ============ -Blafasel, add me +Flask is a microframework and yet it depends on external libraries. There +are various ways how you can install that library and this explains each +way and why there are multiple ways. + +Flask depends on two external libraries: `Werkzeug +`_ and `Jinja2 `_. +The first on is responsible for interfacing WSGI the latter to render +templates. Now you are maybe asking, what is WSGI? WSGI is a standard +in Python that is basically responsible for ensuring that your application +is behaving in a specific way that you can run it on different +environments (for example on a local development server, on an Apache2, on +lighttpd, on Google's appengine or whatever you have in mind). + +So how do you get all that on your computer in no time? The most kick-ass +method is virtualenv, so let's look at that first. + +virtualenv +---------- + +Virtualenv is what you want to use during development and in production if +you have shell access. So first: what does virtualenv do? If you are +like me and you like Python, chances are you want to use it for another +project as well. Now the more projects you have, the more likely it is +that you will be working with different versions of Python itself or a +library involved. Because let's face it: quite often libraries break +backwards compatibility and it's unlikely that your application will +not have any dependencies, that just won't happen. So virtualenv for the +rescue! + +It basically makes it possible to have multiple side-by-side +"installations" of Python, each for your own project. It's not actually +an installation but a clever way to keep things separated. + +So let's see how that works! + +If you are on OS X or Linux chances are that one of the following two +commands will for for you:: + + sudo easy_install virtualenv + +or even better:: + + sudo pip install virtualenv + +Changes are you have virtualenv installed on your system then. Maybe it's +even in your package manager (on ubuntu try ``sudo apt-get install +python-virtualenv``). + +On windows, just installed virtualenv from the `Python Package Index +`_. + +So now that you have virtualenv running just fire up a shell and create +your own environment. I usually create a folder and a `env` folder +within:: + + $ mkdir myproject + $ cd myproject + $ virtualenv env + New python executable in env/bin/python + Installing setuptools............done. + +Now you only have to activate it, whenever you work with it. On OS X and +Linux do the following:: + + $ source env/bin/activate + +If you are a Windows user, the following command is for you:: + + $ env\scripts\activate + +Either way, you should now be using your virtualenv (see how the prompt of +your shell has changed to show the virtualenv). + +Now you can just enter the following command to get Flask activated in +your virtualenv:: + + $ easy_install Flask + +A few seconds later you are good to go. + + +System Wide Installation +------------------------ + +This is possible as well, but I would not recommend it. Just run +`easy_install` with root rights:: + + sudo easy_install Flask + +(Run it in an Admin shell on Windows systems and without the `sudo`). + + +The Drop into Place Version +--------------------------- + +Now I really don't recommend this way on using Flask, but you can do that +of course as well. Download the `dip` zipfile from the website and unzip +it next to your application. From 727c701686c18c4a27ca523eb0c5862d497be24e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 13:40:05 +0200 Subject: [PATCH 0009/3747] And finished documentation for most parts. --- docs/api.rst | 2 + docs/index.rst | 10 +- docs/patterns.rst | 166 ++++++++++++++++++++++++++++++++++ docs/quickstart.rst | 23 +++++ examples/minitwit/minitwit.py | 2 +- 5 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 docs/patterns.rst diff --git a/docs/api.rst b/docs/api.rst index 6c763393..eefdf71c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,3 +1,5 @@ +.. _api: + API === diff --git a/docs/index.rst b/docs/index.rst index 5e5d8622..ddb41da5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,17 @@ Welcome to Flask ================ -Welcome to Flask's documentation. - -Contents: +Welcome to Flask's documentation. This documentation is devided into +different parts. I would suggest to get started with the +:ref:`installation` and then heading over to the :ref:`quickstart`. If +you want to dive into all the internal parts of Flask, check out the +:ref:`api` documentation. Common patterns are described in the +:ref:`patterns` section. .. toctree:: :maxdepth: 2 installation quickstart + patterns api diff --git a/docs/patterns.rst b/docs/patterns.rst new file mode 100644 index 00000000..5047ea73 --- /dev/null +++ b/docs/patterns.rst @@ -0,0 +1,166 @@ +.. _patterns: + +Patterns in Flask +================= + +Certain things are common enough that the changes are high you will find +them in most web applications. For example quite a lot of applications +are using relational databases and user authentication. In that case, +changes are they will open a database connection at the beginning of the +request and get the information of the currently logged in user. At the +end of the request, the database connection is closed again. + +In Flask you can implement such things with the +:meth:`~flask.Flask.request_init` and +:meth:`~flask.Flask.request_shutdown` decorators in combination with the +special :class:`~flask.g` object. + + +Using SQLite 3 with Flask +------------------------- + +So here a simple example how you can use SQLite 3 with Flask:: + + import sqlite3 + from flask import g + + DATABASE = '/path/to/database.db' + + def connect_db(): + return sqlite3.connect(DATABASE) + + @app.request_init + def before_request(): + g.db = connect_db() + + @app.request_shutdown + def after_request(response): + g.db.close() + return response + +Easy Querying +````````````` + +Now in each request handling function you can access `g.db` to get the +current open database connection. To simplify working with SQLite a +helper function can be useful:: + + def query_db(query, args=(), one=False): + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] + return (rv[0] if rv else None) if one else rv + +This handy little function makes working with the database much more +pleasant than it is by just using the raw cursor and connection objects. + +Here is how you can use it:: + + for user in query_db('select * from users'): + print user['username'], 'has the id', user['user_id'] + +Or if you just want a single result:: + + user = query_db('select * from users where username = ?', + [the_username], one=True) + if user is None: + print 'No such user' + else: + print the_username, 'has the id', user['user_id'] + +To pass variable parts to the SQL statement, use a question mark in the +statement and pass in the arguments as a list. Never directly add them to +the SQL statement with string formattings because this makes it possible +to attack the application using `SQL Injections +`_. + +Initial Schemas +``````````````` + +Relational databases need schemas, so applications often ship a +`schema.sql` file that creates the database. It's a good idea to provide +a function that creates the database bases on that schema. This function +can do that for you:: + + from contextlib import closing + + def init_db(): + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + +You can then create such a database from the python shell: + +>>> from yourapplication import init_db +>>> init_db() + +.. _template-inheritance: + +Template Inheritance +-------------------- + +The most powerful part of Jinja is template inheritance. Template inheritance +allows you to build a base "skeleton" template that contains all the common +elements of your site and defines **blocks** that child templates can override. + +Sounds complicated but is very basic. It's easiest to understand it by starting +with an example. + + +Base Template +````````````` + +This template, which we'll call ``layout.html``, defines a simple HTML skeleton +document that you might use for a simple two-column page. It's the job of +"child" templates to fill the empty blocks with content: + +.. sourcecode:: html+jinja + + + + + {% block head %} + + {% block title %}{% endblock %} - My Webpage + {% endblock %} + + +
{% block content %}{% endblock %}
+ + + +In this example, the ``{% block %}`` tags define four blocks that child templates +can fill in. All the `block` tag does is to tell the template engine that a +child template may override those portions of the template. + +Child Template +`````````````` + +A child template might look like this: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block title %}Index{% endblock %} + {% block head %} + {{ super() }} + + {% endblock %} + {% block content %} +

Index

+

+ Welcome on my awesome homepage. + {% endblock %} + +The ``{% extends %}`` tag is the key here. It tells the template engine that +this template "extends" another template. When the template system evaluates +this template, first it locates the parent. The extends tag must be the +first tag in the template. To render the contents of a block defined in +the parent template, use ``{{ super() }}``. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e95e467a..64ef0368 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,3 +1,5 @@ +.. _quickstart: + Quickstart ========== @@ -148,6 +150,22 @@ don't have to deal with that. It will also make sure that ``HEAD`` requests are handled like the RFC demands, so you can completely ignore that part of the HTTP specification. +Static Files +------------ + +Dynamic web applications need static files as well. That's usually where +the CSS and JavaScript files are coming from. Ideally your web server is +configured to serve them for you, but during development Flask can do that +as well. Just create a folder called `static` in your package or next to +your module and it will be available at `/static` on the application. + +To generate URLs to that part of the URL, use the special ``'static'`` URL +name:: + + url_for('static', filename='style.css') + +The file has to be stored on the filesystem as ``static/style.css``. + Rendering Templates ------------------- @@ -205,6 +223,11 @@ Inside templates you also have access to the :class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` objects as well as the :func:`~flask.get_flashed_messages` function. +Templates are especially useful if inheritance is used. If you want to +know how that works, head over to the :ref:`template-inheritance` pattern +documentation. Basically template inheritance makes it possible to keep +certain elements on each page (like header, navigation and footer). + Automatic escaping is enabled, so if name contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be safe HTML (because for example it came from a module that converts wiki diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index ebb304a7..06168746 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -65,7 +65,7 @@ def before_request(): """Make sure we are connected to the database each request and look up the current user so that we know he's there. """ - g.db = sqlite3.connect(DATABASE) + g.db = connect_db() if 'user_id' in session: g.user = query_db('select * from user where user_id = ?', [session['user_id']], one=True) From a9d4ea81da28ee9c894bf9d45adc4cfa9d52fddc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 13:46:12 +0200 Subject: [PATCH 0010/3747] Flashing -> patterns documentation --- docs/patterns.rst | 82 +++++++++++++++++++++++++++++++++++++++++++++ docs/quickstart.rst | 72 +++------------------------------------ 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/docs/patterns.rst b/docs/patterns.rst index 5047ea73..3809f754 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -164,3 +164,85 @@ this template "extends" another template. When the template system evaluates this template, first it locates the parent. The extends tag must be the first tag in the template. To render the contents of a block defined in the parent template, use ``{{ super() }}``. + +.. _message-flashing-pattern: + +Message Flashing +---------------- + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback he will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it next +request and only next request. This is usually combined with a layout +template that does this. + +So here a full example:: + + from flask import flash, redirect, url_for, render_template + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or \ + request.form['password'] != 'secret': + error = 'Invalid credentials' + else: + flash('You were sucessfully logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) + +And here the ``layout.html`` template which does the magic: + +.. sourcecode:: html+jinja + + + My Application + {% with messages = get_flashed_messages() %} + {% if messages %} +

    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block body %}{% endblock %} + +And here the index.html template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Overview

+

Do you want to log in? + {% endblock %} + +And of course the login template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

Login

+ {% if error %} +

Error: {{ error }} + {% endif %} +

+
+
Username: +
+
Password: +
+
+

+

+ {% endblock %} diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 64ef0368..54a421e9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -489,71 +489,7 @@ possible to record a message at the end of a request and access it next request and only next request. This is usually combined with a layout template that does this. -So here a full example:: - - from flask import flash, redirect, url_for, render_template - - @app.route('/') - def index(): - return render_template('index.html') - - @app.route('/login', methods=['GET', 'POST']) - def login(): - error = None - if request.method == 'POST': - if request.form['username'] != 'admin' or \ - request.form['password'] != 'secret': - error = 'Invalid credentials' - else: - flash('You were sucessfully logged in') - return redirect(url_for('index')) - return render_template('login.html', error=error) - -And here the ``layout.html`` template which does the magic: - -.. sourcecode:: html+jinja - - - My Application - {% with messages = get_flashed_messages() %} - {% if messages %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% endif %} - {% endwith %} - {% block body %}{% endblock %} - -And here the index.html template: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

Overview

-

Do you want to log in? - {% endblock %} - -And of course the login template: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

Login

- {% if error %} -

Error: {{ error }} - {% endif %} -

-
-
Username: -
-
Password: -
-
-

-

- {% endblock %} +To flash a message use the :func:`~flask.flash` method, to get hold of the +messages you can use :func:`~flask.get_flashed_messages` which is also +available in the templates. Check out the :ref:`message-flashing-pattern` +for a full example. From 7d6b8e9ab3e738d69e39ad53a7c0f6a552d5284a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 9 Apr 2010 13:56:47 +0200 Subject: [PATCH 0011/3747] Documented non-decorator uses of registation. --- flask.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/flask.py b/flask.py index 9ea9db5c..4bb2d85d 100644 --- a/flask.py +++ b/flask.py @@ -288,6 +288,27 @@ class Flask(object): if session is not None: session.save_cookie(response, self.session_cookie_name) + def add_url_rule(self, endpoint, **options): + """Connects a URL rule. Works exactly like the :meth:`route` + decorator but does not register the view function for the endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following: + + def index(): + pass + app.add_url_rule('index', '/') + app.view_functions['index'] = index + """ + options['endpoint'] = f.__name__ + options.setdefault('methods', ('GET',)) + self.url_map.add(Rule(rule, **options)) + def route(self, rule, **options): """A decorator that is used to register a view function for a given URL rule. Example:: @@ -351,11 +372,8 @@ class Flask(object): setting for this rule. See above. """ def decorator(f): - if 'endpoint' not in options: - options['endpoint'] = f.__name__ - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) - self.view_functions[options['endpoint']] = f + self.add_url_rule(f.__name__, **options) + self.view_functions[f.__name__] = f return f return decorator @@ -366,6 +384,14 @@ class Flask(object): @app.errorhandler(404) def page_not_found(): return 'This page does not exist', 404 + + You can also register a function as error handler without using + the :meth:`errorhandler` decorator. The following example is + equivalent to the one above:: + + def page_not_found(): + return 'This page does not exist', 404 + app.error_handlers[404] = page_not_found """ def decorator(f): self.error_handlers[code] = f From 3d719f35f5c1ee4ce3dc01fb2167ef49c0180cf6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 10 Apr 2010 15:49:15 +0200 Subject: [PATCH 0012/3747] Added docs, fixed some bugs I introduced last commit --- artwork/logo-full.svg | 329 ++++++++++++++++++++++++ docs/_static/flask.png | Bin 0 -> 9925 bytes docs/_static/logo-full.png | Bin 0 -> 23478 bytes docs/_templates/sidebarintro.html | 7 + docs/_templates/sidebarlogo.html | 3 + docs/_themes/flasky/static/flasky.css_t | 239 +++++++++++++++++ docs/_themes/flasky/theme.conf | 3 + docs/becomingbig.rst | 54 ++++ docs/conf.py | 16 +- docs/flaskext.py | 96 +++++++ docs/foreword.rst | 57 ++++ docs/index.rst | 6 + flask.py | 8 +- 13 files changed, 808 insertions(+), 10 deletions(-) create mode 100755 artwork/logo-full.svg create mode 100644 docs/_static/flask.png create mode 100644 docs/_static/logo-full.png create mode 100644 docs/_templates/sidebarintro.html create mode 100644 docs/_templates/sidebarlogo.html create mode 100644 docs/_themes/flasky/static/flasky.css_t create mode 100644 docs/_themes/flasky/theme.conf create mode 100644 docs/becomingbig.rst create mode 100644 docs/flaskext.py create mode 100644 docs/foreword.rst diff --git a/artwork/logo-full.svg b/artwork/logo-full.svg new file mode 100755 index 00000000..43465a4d --- /dev/null +++ b/artwork/logo-full.svg @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/flask.png b/docs/_static/flask.png new file mode 100644 index 0000000000000000000000000000000000000000..5c603cc22c086a9257ee1c91277c42413e6650c4 GIT binary patch literal 9925 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 literal 0 HcmV?d00001 diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png new file mode 100644 index 0000000000000000000000000000000000000000..f255eece8157a0ce6baf7e2e0b297c31ce153405 GIT binary patch literal 23478 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 S^>upy0000About Flask +

+ Flask is a micro webdevelopment framework for Python. You are currently + looking at the documentation of the development version. Things are + not stable yet, but if you have some feedback, + let me know. +

diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html new file mode 100644 index 00000000..3bc7f762 --- /dev/null +++ b/docs/_templates/sidebarlogo.html @@ -0,0 +1,3 @@ + diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t new file mode 100644 index 00000000..05caf2f7 --- /dev/null +++ b/docs/_themes/flasky/static/flasky.css_t @@ -0,0 +1,239 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- flasky theme based on nature theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Georgia', serif; + font-size: 100%; + background-color: #111; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #774117; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E9D1C1; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper { + padding: 20px 0 20px 0; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0 0 10px 0; + margin: 0; + text-align: center; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: 'Georgia', serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #555; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: 'Georgia', serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #003B55; + text-decoration: none; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Georiga', serif; + background-color: #bbb; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 8px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #ddd; } +div.body h3 { font-size: 120%; background-color: #eee; } +div.body h4 { font-size: 110%; background-color: #eee; } +div.body h5 { font-size: 100%; background-color: #eee; } +div.body h6 { font-size: 100%; background-color: #eee; } + +a.headerlink { + color: #c60f0f; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} diff --git a/docs/_themes/flasky/theme.conf b/docs/_themes/flasky/theme.conf new file mode 100644 index 00000000..cb9eb465 --- /dev/null +++ b/docs/_themes/flasky/theme.conf @@ -0,0 +1,3 @@ +[theme] +inherit = basic +stylesheet = flasky.css diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst new file mode 100644 index 00000000..90cc0138 --- /dev/null +++ b/docs/becomingbig.rst @@ -0,0 +1,54 @@ +.. _becomingbig: + +Becoming Big +============ + +Your application is becoming more and more complex? Flask is really not +designed for large scale applications and does not attempt to do so, but +that does not mean you picked the wrong tool in the first place. + +Flask is powered by Werkzeug and Jinja2, two libraries that are in use at +a number of large websites out there and all Flask does is bringing those +two together. Being a microframework, Flask is literally a single file. +What that means for large applications is that it's probably a good idea +to take the code from Flask and put it into a new module within the +applications and expanding on that. + +What Could Be Improved? +----------------------- + +For instance it makes a lot of sense to change the way endpoints (the +names of the functions / URL rules) are handled to also take the module +name into account. Right now the function name is the URL name, but +imagine you have a large applications consisting of multiple components. +In that case, it makes a lot of sense to use dotted names for the URL +endpoints. + +Here some suggestions how Flask can be modified to better accomodate large +scale applications: + +- implement dotted names for URL endpoints +- get rid of the decorator function registering which causes a lot + of troubles for applications that have circular dependencies. It + also requires that the whole application is imported when the system + initializes or certain URLs will not be available right away. +- switch to explicit request object passing. This makes it more to type + (because you now have something to pass around) but it makes it a + whole lot easier to debug hairy situations and to test the code. +- integrate the `Babel`_ i18n package or `SQLAlchemy`_ directl into the + core framework. + +.. _Babel: http://babel.edgewall.org/ +.. _SQLAlchemy: http://www.sqlalchemy.org/ + +Why does not Flask do all that by Default? +------------------------------------------ + +There is a huge difference between a small application that only has to +handle a couple of requests per second and with an overall code complexity +of less than 4000 lines of code or something of larger scale. At one +point it becomes important to integrate external systems, different +storage backends and more. + +If Flask was designed with all these contingencies in mind, it would be a +much more complex framework and less easy to get started with. diff --git a/docs/conf.py b/docs/conf.py index 85c52700..9f417120 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- @@ -80,7 +80,7 @@ exclude_patterns = ['_build'] #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'flaskext.FlaskyStyle' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] @@ -90,7 +90,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +html_theme = 'flasky' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -98,7 +98,7 @@ html_theme = 'nature' #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -108,7 +108,7 @@ html_theme = 'nature' #html_short_title = None # The name of an image file (relative to this directory) to place at the top -# of the sidebar. +# of the sidebar. Do not set, template magic! #html_logo = None # The name of an image file (within the static path) to use as favicon of the @@ -130,7 +130,11 @@ html_static_path = ['_static'] #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +html_sidebars = { + 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', + 'sourcelink.html', 'searchbox.html'] +} # Additional templates that should be rendered to pages, maps page names to # template names. diff --git a/docs/flaskext.py b/docs/flaskext.py new file mode 100644 index 00000000..85331eff --- /dev/null +++ b/docs/flaskext.py @@ -0,0 +1,96 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Multiline: "italic #8f5902", # class: 'cm' + Comment.Preproc: "italic #8f5902", # class: 'cp' + Comment.Single: "italic #8f5902", # class: 'c1' + Comment.Special: "italic #8f5902", # class: 'cs' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#999", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + # since the tango light blue does not show up well in text, we choose + # a pure blue instead. + Number: "bold #0000cf", # class: 'm' + Number.Float: "bold #0000cf", # class: 'mf' + Number.Hex: "bold #0000cf", # class: 'mh' + Number.Integer: "bold #0000cf", # class: 'mi' + Number.Integer.Long: "bold #0000cf", # class: 'il' + Number.Oct: "bold #0000cf", # class: 'mo' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "italic #000000", # class: 'go' + Generic.Prompt: "#8f5902", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/docs/foreword.rst b/docs/foreword.rst new file mode 100644 index 00000000..79243d3b --- /dev/null +++ b/docs/foreword.rst @@ -0,0 +1,57 @@ +Foreword +======== + +Read this before you get started with Flask. This hopefully answers some +questions about the intention of the project, what it aims at and when you +should or should not be using it. + +What does Micro Mean? +--------------------- + +The micro in microframework for me means on the one hand being small in +size, complexity but on the other hand also that the complexity of the +applications that are written with these frameworks do not exceed a +certain size. A microframework like Flask sacrifices a few things in +order to be approachable and to be as concise as possible. + +For example Flask uses thread local objects internally so that you don't +have to pass objects around from function to function within a request in +order to stay threadsafe. While this is a really easy approach and saves +you a lot of time, it also does not scale well to large applications. +It's especially painful for more complex unittests and when you suddenly +have to deal with code being executed outside of the context of a request +(for example if you have cronjobs). + +Flask provides some tools to deal with the downsides of this approach but +the core problem of this approach obviously stays. It is also based on +convention over configuration which means that a lot of things are +preconfigured in Flask and will work well for smaller applications but not +so much for larger ones (where and how it looks for templates, static +files etc.) + +But don't worry if your application suddenly grows larger than it was +initially and you're afraid Flask might not grow with it. Even with +larger frameworks you sooner or later will find out that you need +something the framework just cannot do for you without modification. +If you are ever in that situation, check out the :ref:`becomingbig` +chapter. + +Target Audience +--------------- + +Is Flask for you? Is your application small-ish (less than 4000 lines of +Python code) and does not depend on too complex database structures, Flask +is the Framework for you. It was designed from the ground up to be easy +to use, based on established principles, good intentions and on top of two +established libraries in widespread usage. + +Flask serves two purposes: it's an example of how to create a minimal and +opinionated framework on top of Werkzeug to show how this can be done, and +to provide people with a simple tool to prototype larger applications or +to implement small and medium sized applications. + +If you suddenly discover that your application grows larger than +originally intended, head over to the :ref:`becomingbig` section to see +some possible solutions for larger applications. + +Satisfied? Then head over to the :ref:`installation`. diff --git a/docs/index.rst b/docs/index.rst index ddb41da5..74397dbe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,10 @@ Welcome to Flask ================ +.. image:: _static/logo-full.png + :alt: The Flask Logo with Subtitle + :align: right + Welcome to Flask's documentation. This documentation is devided into different parts. I would suggest to get started with the :ref:`installation` and then heading over to the :ref:`quickstart`. If @@ -11,7 +15,9 @@ you want to dive into all the internal parts of Flask, check out the .. toctree:: :maxdepth: 2 + foreword installation quickstart patterns api + becomingbig diff --git a/flask.py b/flask.py index 4bb2d85d..344cae3b 100644 --- a/flask.py +++ b/flask.py @@ -288,7 +288,7 @@ class Flask(object): if session is not None: session.save_cookie(response, self.session_cookie_name) - def add_url_rule(self, endpoint, **options): + def add_url_rule(self, rule, endpoint, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator but does not register the view function for the endpoint. @@ -298,14 +298,14 @@ class Flask(object): def index(): pass - Is equivalent to the following: + Is equivalent to the following:: def index(): pass app.add_url_rule('index', '/') app.view_functions['index'] = index """ - options['endpoint'] = f.__name__ + options['endpoint'] = endpoint options.setdefault('methods', ('GET',)) self.url_map.add(Rule(rule, **options)) @@ -372,7 +372,7 @@ class Flask(object): setting for this rule. See above. """ def decorator(f): - self.add_url_rule(f.__name__, **options) + self.add_url_rule(rule, f.__name__, **options) self.view_functions[f.__name__] = f return f return decorator From 9dfab582d065521e9350327801ed2c5ced1c8fa1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 10 Apr 2010 16:00:13 +0200 Subject: [PATCH 0013/3747] Updated README --- README | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README b/README index 9b82ae29..f3a8a03e 100644 --- a/README +++ b/README @@ -11,7 +11,20 @@ and Jinja2. It's intended for small scale applications and was development with best intentions in mind. - ~ Is it ready? + ~ Is it ready? Nope, this is still work in progress, but I am happy to accept patches and improvements already. + + ~ What do I need? + + Currently the development versions of Jinja2 and Werkzeug. + Because of Flask I did some refactoring in Werkzeug and + Jinja2 and the next versions (out April 10th) will contain + these changes. + + ~ Where are the docs? + + Go to http://flask.pocoo.org/ for a prebuild version of + the current documentation. Otherwise build them yourself + from the sphinx sources in the docs folder. From 8e9bd5f009bb8756a2a7d76a4b792323eeee49d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 10 Apr 2010 16:01:14 +0200 Subject: [PATCH 0014/3747] Fixed a typo --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 74397dbe..290656ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ Welcome to Flask :alt: The Flask Logo with Subtitle :align: right -Welcome to Flask's documentation. This documentation is devided into +Welcome to Flask's documentation. This documentation is divided into different parts. I would suggest to get started with the :ref:`installation` and then heading over to the :ref:`quickstart`. If you want to dive into all the internal parts of Flask, check out the From 4e1b3e1134eb478563bc434d794085d0a412cb84 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 10 Apr 2010 17:26:49 +0200 Subject: [PATCH 0015/3747] set secret key :) --- docs/quickstart.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 54a421e9..45a3f0f6 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -478,6 +478,9 @@ sessions work:: # remove the username from the session if its there session.pop('username', None) + # set the secret key. keep this really secret: + app.secret_key = 'the secret key' + Message Flashing ---------------- From 05f36c7f7e2df36ee28f90d99632162579b1287c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 02:20:10 +0200 Subject: [PATCH 0016/3747] Heavily improved documentation --- docs/_templates/sidebarintro.html | 5 + docs/_themes/flasky/static/flasky.css_t | 94 +++++++-- docs/api.rst | 8 +- docs/deploying.rst | 266 ++++++++++++++++++++++++ docs/flaskext.py | 20 +- docs/foreword.rst | 9 + docs/index.rst | 1 + docs/installation.rst | 43 +++- docs/quickstart.rst | 11 +- flask.py | 73 ++++++- 10 files changed, 486 insertions(+), 44 deletions(-) create mode 100644 docs/deploying.rst diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 46e01d5c..06bd737e 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -5,3 +5,8 @@ not stable yet, but if you have some feedback, let me know.

+

Useful Links

+ diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index 05caf2f7..bd829c0c 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -16,7 +16,7 @@ body { font-family: 'Georgia', serif; font-size: 100%; - background-color: #111; + background-color: #555; color: #555; margin: 0; padding: 0; @@ -47,15 +47,13 @@ div.body { } div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; + color: #ccc; + padding: 10px; + font-size: 0.8em; } div.footer a { - color: #444; + color: white; text-decoration: underline; } @@ -138,7 +136,7 @@ div.sphinxsidebar input[type=text]{ /* -- body styles ----------------------------------------------------------- */ a { - color: #003B55; + color: #004B6B; text-decoration: none; } @@ -159,7 +157,7 @@ div.body h6 { color: #212224; margin: 30px 0px 10px 0px; padding: 8px 0 5px 10px; - text-shadow: 0px 1px 0 white + text-shadow: 0px 1px 0 white; } div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } @@ -170,14 +168,14 @@ div.body h5 { font-size: 100%; background-color: #eee; } div.body h6 { font-size: 100%; background-color: #eee; } a.headerlink { - color: #c60f0f; + color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { - background-color: #c60f0f; - color: white; + color: #444; + background: #eaeaea; } div.body p, div.body dd, div.body li { @@ -218,22 +216,78 @@ p.admonition-title { p.admonition-title:after { content: ":"; } + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +tt.descname, tt.descclassname { + font-size: 0.95em; + -webkit-box-shadow: none; + -moz-box-shadow: none; +} + +tt.descname { + padding-right: 0.08em; +} + +table.docutils { + border: 1px solid #888; + -webkit-box-shadow: 2px 2px 1px #d8d8d8; + -moz-box-shadow: 2px 2px 1px #d8d8d8; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list { + border: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} pre { + background: #FDFDFD; padding: 10px; color: #222; - line-height: 1.2em; - border: 1px solid #C6C9CB; - font-size: 1.1em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; + line-height: 1.3em; + border: 1px solid #f9f9f9; + margin: 1.5em 3px 1.5em 0; + -webkit-box-shadow: 2px 2px 1px #d8d8d8; + -moz-box-shadow: 2px 2px 1px #d8d8d8; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ - font-size: 1.1em; - font-family: monospace; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt.xref, a tt { + background-color: #FBFBFB; +} + +a:hover tt { + background: #EEE; +} + +div.document + div.related { + background: #aaa; +} + +div.document + div.related a { + color: white; } diff --git a/docs/api.rst b/docs/api.rst index eefdf71c..98614ead 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -163,7 +163,13 @@ Useful Functions and Classes .. autofunction:: url_for -.. autofunction:: abort +.. function:: abort(code) + + Raises an :exc:`~werkzeug.exception.HTTPException` for the given + status code. For example to abort request handling with a page not + found exception, you would call ``abort(404)``. + + :param code: the HTTP error code. .. autofunction:: redirect diff --git a/docs/deploying.rst b/docs/deploying.rst new file mode 100644 index 00000000..345d7ee9 --- /dev/null +++ b/docs/deploying.rst @@ -0,0 +1,266 @@ +Deployment Options +================== + +Depending on what you have available there are multiple ways to run Flask +applications. A very common method is to use the builtin server during +development and maybe behind a proxy for simple applications, but there +are more options available. + +If you have a different WSGI server look up the server documentation about +how to use a WSGI app with it. Just remember that your application object +is the actual WSGI application. + + +FastCGI +------- + +A very popular deployment setup on servers like `lighttpd`_ and `nginx`_ +is FastCGI. To use your WSGI application with any of them you will need +a FastCGI server first. + +The most popular one is `flup`_ which we will use for this guide. Make +sure to have it installed. + +Creating a `.fcgi` file +``````````````````````` + +First you need to create the FastCGI server file. Let's call it +`yourapplication.fcgi`:: + + #!/usr/bin/python + from flup.server.fcgi import WSGIServer + from yourapplication import app + + WSGIServer(app).run() + +This is enough for Apache to work, however lighttpd and nginx need a +socket to communicate with the FastCGI server. For that to work you +need to pass the path to the socket to the +:class:`~flup.server.fcgi.WSGIServer`:: + + WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() + +The path has to be the exact same path you define in the server +config. + +Save the `yourapplication.fcgi` file somewhere you will find it again. +It makes sense to have that in `/var/www/yourapplication` or something +similar. + +Make sure to set the executable bit on that file so that the servers +can execute it:: + + # chmod +x /var/www/yourapplication/yourapplication.fcgi + +Configuring lighttpd +```````````````````` + +A basic FastCGI configuration for lighttpd looks like that:: + + fastcgi.server = ("/yourapplication" => + "yourapplication" => ( + "socket" => "/tmp/yourapplication-fcgi.sock", + "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", + "check-local" => "disable" + ) + ) + +This configuration binds the application to `/yourapplication`. If you +want the application to work in the URL root you have to work around a +lighttpd bug with the `~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. + +Make sure to apply it only if you are mounting the application the URL +root. + +Configuring nginx +````````````````` + +Installing FastCGI applications on nginx is a bit tricky because by default +some FastCGI parameters are not properly forwarded. + +A basic FastCGI configuration for nginx looks like this:: + + location /yourapplication/ { + include fastcgi_params; + if ($uri ~ ^/yourapplication/(.*)?) { + set $path_url $1; + } + fastcgi_param PATH_INFO $path_url; + fastcgi_param SCRIPT_NAME /yourapplication; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +This configuration binds the application to `/yourapplication`. If you want +to have it in the URL root it's a bit easier because you don't have to figure +out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: + + location /yourapplication/ { + include fastcgi_params; + fastcgi_param PATH_INFO $fastcgi_script_name; + fastcgi_param SCRIPT_NAME ""; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +Since Nginx doesn't load FastCGI apps, you have to do it by yourself. You +can either write an `init.d` script for that or execute it inside a screen +session:: + + $ screen + $ /var/www/yourapplication/yourapplication.fcgi + +Debugging +````````` + +FastCGI deployments tend to be hard to debug on most webservers. Very often the +only thing the server log tells you is something along the lines of "premature +end of headers". In order to debug the application the only thing that can +really give you ideas why it breaks is switching to the correct user and +executing the application by hand. + +This example assumes your application is called `application.fcgi` and that your +webserver user is `www-data`:: + + $ su www-data + $ cd /var/www/yourapplication + $ python application.fcgi + Traceback (most recent call last): + File "yourapplication.fcg", line 4, in + ImportError: No module named yourapplication + +In this case the error seems to be "yourapplication" not being on the python +path. Common problems are: + +- relative paths being used. Don't rely on the current working directory +- the code depending on environment variables that are not set by the + web server. +- different python interpreters being used. + +.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: http://nginx.net/ +.. _flup: http://trac.saddi.com/flup + + +mod_wsgi (Apache) +----------------- + +If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. + +.. _Apache: http://httpd.apache.org/ + +Installing `mod_wsgi` +````````````````````` + +If you don't have `mod_wsgi` installed yet you have to either install it using +a package manager or compile it yourself. + +The mod_wsgi `installation instructions`_ cover installation instructions for +source installations on UNIX systems. + +If you are using ubuntu / debian you can apt-get it and activate it as follows:: + + # apt-get install libapache2-mod-wsgi + +On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using +pkg_add:: + + # pkg_add -r mod_wsgi + +If you are using pkgsrc you can install `mod_wsgi` by compiling the +`www/ap2-wsgi` package. + +If you encounter segfaulting child processes after the first apache reload you +can safely ignore them. Just restart the server. + +Creating a `.wsgi` file +``````````````````````` + +To run your application you need a `yourapplication.wsgi` file. This file +contains the code `mod_wsgi` is executing on startup to get the application +object. The object called `application` in that file is then used as +application. + +For most applications the following file should be sufficient:: + + from yourapplication import app as application + +If you don't have a factory function for application creation but a singleton +instance you can directly import that one as `application`. + +Store that file somewhere where you will find it again (eg: +`/var/www/yourapplication`) and make sure that `yourapplication` and all +the libraries that are in use are on the python load path. If you don't +want to install it system wide consider using a `virtual python`_ instance. + +Configuring Apache +`````````````````` + +The last thing you have to do is to create an Apache configuration file for +your application. In this example we are telling `mod_wsgi` to execute the +application under a different user for security reasons: + +.. sourcecode:: apache + + + ServerName example.com + + WSGIDaemonProcess yourapplication user=user1 group=group1 processes=1 threads=5 + WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi + + + WSGIProcessGroup yourapplication + WSGIApplicationGroup %{GLOBAL} + Order deny,allow + Allow from all + + + +For more information consult the `mod_wsgi wiki`_. + +.. _mod_wsgi: http://code.google.com/p/modwsgi/ +.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide +.. _virtual python: http://pypi.python.org/pypi/virtualenv +.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ + + +CGI +--- + +If all other deployment methods do not work, CGI will work for sure. CGI +is supported by all major browsers but usually has a less-than-optimal +performance. + +This is also the way you can use a Flask application on Google's +`AppEngine`_, there however the execution does happen in a CGI-like +environment. The application's performance is unaffected because of that. + +.. _AppEngine: http://code.google.com/appengine/ + +Creating a `.cgi` file +`````````````````````` + +First you need to create the CGI application file. Let's call it +`yourapplication.cgi`:: + + #!/usr/bin/python + from wsgiref.handlers import CGIHandler + from yourapplication import app + + CGIHandler().run(app) + +If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python +2.5 and higher ship this as part of the standard library. + +Server Setup +```````````` + +Usually there are two ways to configure the server. Either just copy the +`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to +rewrite the URL) or let the server point to the file directly. + +In Apache for example you can put a like like this into the config: + +.. sourcecode:: apache + + ScriptName /app /path/to/the/application.cgi + +For more information consult the documentation of your webserver. diff --git a/docs/flaskext.py b/docs/flaskext.py index 85331eff..33f47449 100644 --- a/docs/flaskext.py +++ b/docs/flaskext.py @@ -16,10 +16,7 @@ class FlaskyStyle(Style): Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' - Comment.Multiline: "italic #8f5902", # class: 'cm' - Comment.Preproc: "italic #8f5902", # class: 'cp' - Comment.Single: "italic #8f5902", # class: 'c1' - Comment.Special: "italic #8f5902", # class: 'cs' + Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' @@ -43,7 +40,7 @@ class FlaskyStyle(Style): Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#999", # class: 'nd' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' @@ -57,14 +54,7 @@ class FlaskyStyle(Style): Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - # since the tango light blue does not show up well in text, we choose - # a pure blue instead. - Number: "bold #0000cf", # class: 'm' - Number.Float: "bold #0000cf", # class: 'mf' - Number.Hex: "bold #0000cf", # class: 'mh' - Number.Integer: "bold #0000cf", # class: 'mi' - Number.Integer.Long: "bold #0000cf", # class: 'il' - Number.Oct: "bold #0000cf", # class: 'mo' + Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' @@ -88,8 +78,8 @@ class FlaskyStyle(Style): Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "italic #000000", # class: 'go' - Generic.Prompt: "#8f5902", # class: 'gp' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' diff --git a/docs/foreword.rst b/docs/foreword.rst index 79243d3b..580cf37d 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -36,6 +36,15 @@ something the framework just cannot do for you without modification. If you are ever in that situation, check out the :ref:`becomingbig` chapter. +A Framework and An Example +-------------------------- + +Flask is not only a microframework, it is also an example. Based on +Flask, there will be a series of blog posts that explain how to create a +framework. Flask itself is just one way to implement a framework on top +of existing libraries. Unlike many other microframeworks Flask does not +try to implement anything on its own, it reuses existing code. + Target Audience --------------- diff --git a/docs/index.rst b/docs/index.rst index 290656ea..81528203 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,4 +20,5 @@ you want to dive into all the internal parts of Flask, check out the quickstart patterns api + deploying becomingbig diff --git a/docs/installation.rst b/docs/installation.rst index b8375cd7..89e5a680 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -41,18 +41,19 @@ So let's see how that works! If you are on OS X or Linux chances are that one of the following two commands will for for you:: - sudo easy_install virtualenv + $ sudo easy_install virtualenv or even better:: - sudo pip install virtualenv + $ sudo pip install virtualenv Changes are you have virtualenv installed on your system then. Maybe it's even in your package manager (on ubuntu try ``sudo apt-get install python-virtualenv``). -On windows, just installed virtualenv from the `Python Package Index -`_. +If you are on Windows and missing the `easy_install` command you have to +install it first. Check the :ref:`windows-easy-install` section for more +information about how to do that. So now that you have virtualenv running just fire up a shell and create your own environment. I usually create a folder and a `env` folder @@ -101,3 +102,37 @@ The Drop into Place Version Now I really don't recommend this way on using Flask, but you can do that of course as well. Download the `dip` zipfile from the website and unzip it next to your application. + +.. _windows-easy-install: + +`easy_install` on Windows +------------------------- + +On Windows installation of `easy_install` is a little bit tricker because +on Windows slightly different rules apply, but it's not a biggy. The +easiest way to accomplish that is downloading the `ez_setup.py`_ file and +running it. (Double clicking should do the trick) + +Once you have done that it's important to add the `easy_install` command +and other Python scripts to the path. To do that you have to add the +Python installation's Script folder to the `PATH` variable. + +To do that, click right on your "Computer" desktop icon and click +"Properties". On Windows Vista and Windows 7 then click on "Advanced System +settings", on Windows XP click on the "Advanced" tab instead. Then click +on the "Environment variables" button and double click on the "Path" +variable in the "System variables" section. + +There append the path of your Python interpreter's Script folder to the +end of the last (make sure you delimit it from existing values with a +semicolon). Assuming you are using Python 2.6 on the default path, add +the following value:: + + ;C:\Python26\Scripts + +Then you are done. To check if it worked, open the cmd and execute +"easy_install". If you have UAC enabled it should prompt you for admin +privileges. + + +.. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 45a3f0f6..429d7ab3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -23,9 +23,14 @@ A minimal Flask application looks something like that:: if __name__ == '__main__': app.run() -If you now start that application with your Python interpreter and head -over to `http://localhost:5000/ `_, you should see -your hello world application. +Just save it as `hello.py` or something similar and run it with your +Python interpreter:: + + $ python hello.py + * Running on http://localhost:5000/ + +Head over to `http://localhost:5000/ `_, you should +see your hello world greeting. So what did that code do? diff --git a/flask.py b/flask.py index 344cae3b..1ea239f8 100644 --- a/flask.py +++ b/flask.py @@ -79,6 +79,8 @@ def flash(message): """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. + + :param message: the message to be flashed. """ session['_flashes'] = (session.get('_flashes', [])) + [message] @@ -98,6 +100,10 @@ def get_flashed_messages(): def render_template(template_name, **context): """Renders a template from the template folder with the given context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. """ current_app.update_template_context(context) return current_app.jinja_env.get_template(template_name).render(context) @@ -106,6 +112,11 @@ def render_template(template_name, **context): def render_template_string(source, **context): """Renders a template from the given template source string with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. """ current_app.update_template_context(context) return current_app.jinja_env.from_string(source).render(context) @@ -222,6 +233,9 @@ class Flask(object): def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. + + :param context: the context as a dictionary that is updated in place + to add extra variables. """ reqctx = _request_ctx_stack.top context['request'] = reqctx.request @@ -232,6 +246,13 @@ class Flask(object): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. + + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` + to have the server available externally as well. + :param port: the port of the webserver + :param options: the options to be forwarded to the underlying + Werkzeug server. See :func:`werkzeug.run_simple` + for more information. """ from werkzeug import run_simple if 'debug' in options: @@ -268,6 +289,9 @@ class Flask(object): with app.open_resource('schema.sql') as f: contents = f.read() do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. """ return pkg_resources.resource_stream(self.package_name, resource) @@ -275,6 +299,8 @@ class Flask(object): """Creates or opens a new session. Default implementation stores all session data in a signed cookie. This requires that the :attr:`secret_key` is set. + + :param request: an instance of :attr:`request_class`. """ key = self.secret_key if key is not None: @@ -284,6 +310,11 @@ class Flask(object): def save_session(self, session, response): """Saves the session if it needs updates. For the default implementation, check :meth:`open_session`. + + :param session: the session to be saved (a + :class:`~werkzeug.contrib.securecookie.SecureCookie` + object) + :param request: an instance of :attr:`response_class` """ if session is not None: session.save_cookie(response, self.session_cookie_name) @@ -304,6 +335,13 @@ class Flask(object): pass app.add_url_rule('index', '/') app.view_functions['index'] = index + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object """ options['endpoint'] = endpoint options.setdefault('methods', ('GET',)) @@ -363,6 +401,7 @@ class Flask(object): The :meth:`route` decorator accepts a couple of other arguments as well: + :param rule: the URL rule as string :param methods: a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). @@ -370,6 +409,8 @@ class Flask(object): subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. + :param options: other options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. """ def decorator(f): self.add_url_rule(rule, f.__name__, **options) @@ -392,6 +433,8 @@ class Flask(object): def page_not_found(): return 'This page does not exist', 404 app.error_handlers[404] = page_not_found + + :param code: the code as integer for the handler """ def decorator(f): self.error_handlers[code] = f @@ -440,6 +483,22 @@ class Flask(object): def make_response(self, rv): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. + + The following types are allowd for `rv`: + + ======================= =========================================== + :attr:`response_class` the object is returned unchanged + :class:`str` a response object is created with the + string as body + :class:`unicode` a response object is created with the + string encoded to utf-8 as body + :class:`tuple` the response object is created with the + contents of the tuple as arguments + a WSGI function the function is called as WSGI application + and buffered as response object + ======================= =========================================== + + :param rv: the return value from the view function """ if isinstance(rv, self.response_class): return rv @@ -464,6 +523,10 @@ class Flask(object): def process_response(self, response): """Can be overridden in order to modify the response object before it's sent to the WSGI server. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. """ session = _request_ctx_stack.top.session if session is not None: @@ -477,6 +540,11 @@ class Flask(object): `__call__` so that middlewares can be applied: app.wsgi_app = MyMiddleware(app.wsgi_app) + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response """ with self.request_context(environ): rv = self.preprocess_request() @@ -497,6 +565,8 @@ class Flask(object): with app.request_context(environ): do_something_with(request) + + :params environ: a WSGI environment """ _request_ctx_stack.push(_RequestContext(self, environ)) try: @@ -506,7 +576,8 @@ class Flask(object): def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see - :func:`werkzeug.create_environ` for more information). + :func:`werkzeug.create_environ` for more information, this + function accepts the same arguments). """ return self.request_context(create_environ(*args, **kwargs)) From 1fff3e598126a084348ec2c112fdd3bc6b9a1ee0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 02:42:13 +0200 Subject: [PATCH 0017/3747] Fixed a doc display bug and setup.py workaround for dev version. --- docs/_themes/flasky/static/flasky.css_t | 8 ++++---- docs/deploying.rst | 3 ++- setup.py | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index bd829c0c..b77f3c30 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -86,7 +86,7 @@ div.sphinxsidebarwrapper p.logo { div.sphinxsidebar h3, div.sphinxsidebar h4 { - font-family: 'Georgia', serif; + font-family: 'Garamond', 'Georgia', serif; color: #222; font-size: 1.2em; font-weight: normal; @@ -96,7 +96,7 @@ div.sphinxsidebar h4 { text-shadow: 1px 1px 0 white } -div.sphinxsidebar h4{ +div.sphinxsidebar h4 { font-size: 1.1em; } @@ -151,12 +151,12 @@ div.body h3, div.body h4, div.body h5, div.body h6 { - font-family: 'Georiga', serif; + font-family: 'Garamond', 'Georiga', serif; background-color: #bbb; font-weight: normal; color: #212224; margin: 30px 0px 10px 0px; - padding: 8px 0 5px 10px; + padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white; } diff --git a/docs/deploying.rst b/docs/deploying.rst index 345d7ee9..ea812c17 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -67,7 +67,8 @@ A basic FastCGI configuration for lighttpd looks like that:: This configuration binds the application to `/yourapplication`. If you want the application to work in the URL root you have to work around a -lighttpd bug with the `~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. +lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` +middleware. Make sure to apply it only if you are mounting the application the URL root. diff --git a/setup.py b/setup.py index 72ef584e..c4aee59d 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,9 @@ setup( modules=['flask'], zip_safe=False, platforms='any', - install_requires=[ # yes, as of now we need the development versions - 'Werkzeug==dev', - 'Jinja2==dev', + install_requires=[ + ## disabled until release, install yourself + # 'Werkzeug', + # 'Jinja2' ] ) From 574e81f9c8bbdc41958e1e7a7613633b091101f8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 02:46:44 +0200 Subject: [PATCH 0018/3747] Fixed a bug in setup.py --- docs/quickstart.rst | 5 ++++- setup.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 429d7ab3..c5bb19d3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -24,7 +24,10 @@ A minimal Flask application looks something like that:: app.run() Just save it as `hello.py` or something similar and run it with your -Python interpreter:: +Python interpreter. Make sure to not call your application `flask.py` +because this would conflict with Flask itself. + +:: $ python hello.py * Running on http://localhost:5000/ diff --git a/setup.py b/setup.py index c4aee59d..a0bddf4e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( author='Armin Ronacher', author_email='armin.ronacher@active-4.com', description='A microframework based on Werkzeug, Jinja2 and good intentions', - modules=['flask'], + py_modules=['flask'], zip_safe=False, platforms='any', install_requires=[ From df929c6c77ea5490a969275e45075c29711a498c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 02:58:13 +0200 Subject: [PATCH 0019/3747] further improved documentation --- docs/quickstart.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c5bb19d3..f271d9a1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -52,6 +52,30 @@ So what did that code do? makes sure the server only runs if the script is executed directly from the Python interpreter and not used as imported module. +To stop the server, hit control-C. + + +Debug Mode +---------- + +Now that :meth:`~flask.Flask.run` method is nice to start a local +development server, but you would have to restart it manually after each +change you do to code. That is not very nice and Flask can do better. If +you enable the debug support the server will reload itself on code changes +and also provide you with a helpful debugger if things go wrong. + +There are two ways to enable debugging. Either set that flag on the +applciation object:: + + app.debug = True + app.run() + +Or pass it to run:: + + app.run(debug=True) + +Both will have exactly the same effect. + Routing ------- From 8171da88a49eba769d313a51591ee8575be59762 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:16:42 +0200 Subject: [PATCH 0020/3747] Described HTTP methods --- docs/quickstart.rst | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f271d9a1..f1715498 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -161,6 +161,18 @@ explained below. It basically tells flask to think we are handling a request even though we are not, we are in an interactive Python shell. Have a look at the explanation below. :ref:`context-locals`). +Why would you want to build URLs instead of hardcoding them in your +templates? There are three good reasons for this: + +1. reversing is often more descriptive than hardcoding the URLs. Also and + more importantly you can change URLs in one go without having to change + the URLs all over the place. +2. URL building will handle escaping of special characters and unicode + data transparently for you, you don't have to deal with that. +3. If your application is placed outside the URL root (so say in + ``/myapplication`` instead of ``/``), :func:`~flask.url_for` will + handle that properly for you. + HTTP Methods ```````````` @@ -182,6 +194,51 @@ don't have to deal with that. It will also make sure that ``HEAD`` requests are handled like the RFC demands, so you can completely ignore that part of the HTTP specification. +You have no idea what an HTTP method is? Worry not, here quick +introduction in HTTP methods and why they matter: + +The HTTP method (also often called "the verb") tells the server what the +clients wants to *do* with the requested page. The following methods are +very common: + +`GET` + The Browser tells the server: just *get* me the information stored on + that page and send them to me. This is probably the most common + method. + +`HEAD` + The Browser tells the server: get me the information, but I am only + interested in the *headers*, not the content of the page. An + application is supposed to handle that as if a `GET` request was + received but not deliver the actual contents. In Flask you don't have + to deal with that at all, the underlying Werkzeug library handles that + for you. + +`POST` + The browser tells the server that it wants to *post* some new + information to that URL and that the server must ensure the data is + stored and only stored once. + +`PUT` + Similar to `POST` but the server might trigger the store procedure + multiple times by overwriting the old values more than once. Now you + might be asking why this is any useful, but there are some good + reasons to do that. Consider the connection is lost during + transmission, in that situation a system between the browser and the + server might sent the request safely a second time without breaking + things. With `POST` that would not be possible because it might only + be triggered once. + +`DELETE` + Remove the information that the given location. + +Now the interesting part is that in HTML4 and XHTML1, the only methods a +form might submit to the server are `GET` and `POST`. But with JavaScript +and future HTML standards you can use other methods as well. Furthermore +HTTP became quite popular lately and there are more things than browsers +that are speaking HTTP. (Your revision control system for instance might +speak HTTP) + Static Files ------------ From 85ca089dde8814c4a42ad4574cddce25d60e5e25 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:18:36 +0200 Subject: [PATCH 0021/3747] might -> must --- docs/quickstart.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f1715498..49700fa7 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -217,7 +217,8 @@ very common: `POST` The browser tells the server that it wants to *post* some new information to that URL and that the server must ensure the data is - stored and only stored once. + stored and only stored once. This is how HTML forms are usually + transmitting data to the server. `PUT` Similar to `POST` but the server might trigger the store procedure @@ -226,7 +227,7 @@ very common: reasons to do that. Consider the connection is lost during transmission, in that situation a system between the browser and the server might sent the request safely a second time without breaking - things. With `POST` that would not be possible because it might only + things. With `POST` that would not be possible because it must only be triggered once. `DELETE` From 4e7aa9e7f7a86ee0ea4b6801694c13151518cb52 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:22:26 +0200 Subject: [PATCH 0022/3747] Linked HTTP --- docs/quickstart.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 49700fa7..e6f2e153 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -177,10 +177,10 @@ templates? There are three good reasons for this: HTTP Methods ```````````` -HTTP knows different methods to access URLs. By default a route only -answers to ``GET`` requests, but that can be changed by providing the -`methods` argument to the :meth:`~flask.Flask.route` decorator. Here some -examples:: +HTTP (the protocol web applications are speaking) knows different methods +to access URLs. By default a route only answers to `GET` requests, but +that can be changed by providing the `methods` argument to the +:meth:`~flask.Flask.route` decorator. Here some examples:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -189,10 +189,11 @@ examples:: else: show_the_login_form() -If ``GET`` is present, ``HEAD`` will be added automatically for you. You -don't have to deal with that. It will also make sure that ``HEAD`` -requests are handled like the RFC demands, so you can completely ignore -that part of the HTTP specification. +If `GET` is present, `HEAD` will be added automatically for you. You +don't have to deal with that. It will also make sure that `HEAD` requests +are handled like the `HTTP RFC`_ (the document describing the HTTP +protocol) demands, so you can completely ignore that part of the HTTP +specification. You have no idea what an HTTP method is? Worry not, here quick introduction in HTTP methods and why they matter: @@ -240,6 +241,8 @@ HTTP became quite popular lately and there are more things than browsers that are speaking HTTP. (Your revision control system for instance might speak HTTP) +.. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt + Static Files ------------ From 4671429a509094f36e2294055340197091d39979 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:41:01 +0200 Subject: [PATCH 0023/3747] Added a security section to the foreword and a footnote to the g variable --- docs/_themes/flasky/static/flasky.css_t | 12 ++++++++++- docs/foreword.rst | 28 +++++++++++++++++++++++++ docs/patterns.rst | 2 ++ docs/quickstart.rst | 9 ++++++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index b77f3c30..15495a06 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -243,12 +243,18 @@ table.docutils td, table.docutils th { padding: 0.25em 0.7em; } -table.field-list { +table.field-list, table.footnote { border: none; -webkit-box-shadow: none; -moz-box-shadow: none; } +table.footnote { + border: 1px solid #eee; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + table.field-list th { padding: 0 0.8em 0 0; } @@ -256,6 +262,10 @@ table.field-list th { table.field-list td { padding: 0; } + +table.footnote td { + padding: 0.5em; +} pre { background: #FDFDFD; diff --git a/docs/foreword.rst b/docs/foreword.rst index 580cf37d..6b40921f 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -45,6 +45,34 @@ framework. Flask itself is just one way to implement a framework on top of existing libraries. Unlike many other microframeworks Flask does not try to implement anything on its own, it reuses existing code. +Web Development is Dangerous +---------------------------- + +I'm not even joking. Well, maybe a little. If you write a web +application you are probably allowing users to register and leave their +data on your server. The users are entrusting you with data. And even if +you are the only user that might leave data in your application, you still +want that data to be stored in a secure manner. + +Unfortunately there are many ways security of a web application can be +compromised. Flask protects you against one of the most common security +problems of modern web applications: cross site scripting (XSS). Unless +you deliberately mark insecure HTML as secure Flask (and the underlying +Jinja2 template engine) have you covered. But there are many more ways to +cause security problems. + +Whenever something is dangerous where you have to watch out, the +documentation will tell you so. Some of the security concerns of web +development are far more complex than one might think and often we all end +up in situations where we think "well, this is just far fetched, how could +that possibly be exploited" and then an intelligent guy comes along and +figures a way out to exploit that application. And don't think, your +application is not important enough for hackers to take notice. Depending +ont he kind of attack, chances are there are automated botnets out there +trying to figure out how to fill your database with viagra adverisments. + +So always keep that in mind when doing web development. + Target Audience --------------- diff --git a/docs/patterns.rst b/docs/patterns.rst index 3809f754..c7b4769a 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -16,6 +16,8 @@ In Flask you can implement such things with the special :class:`~flask.g` object. +.. _database-pattern: + Using SQLite 3 with Flask ------------------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e6f2e153..de8ce039 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -313,8 +313,8 @@ Here an example template: {% endif %} Inside templates you also have access to the :class:`~flask.request`, -:class:`~flask.session` and :class:`~flask.g` objects as well as the -:func:`~flask.get_flashed_messages` function. +:class:`~flask.session` and :class:`~flask.g` [#]_ objects +as well as the :func:`~flask.get_flashed_messages` function. Templates are especially useful if inheritance is used. If you want to know how that works, head over to the :ref:`template-inheritance` pattern @@ -338,6 +338,11 @@ Markup(u'<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() u'Marked up \xbb HTML' +.. [#] Unsure what that :class:`~flask.g` object is? It's something you + can store information on yourself, check the documentation of that + object (:class:`~flask.g`) and the :ref:`database-pattern` for more + information. + Accessing Request Data ---------------------- From d78b04a3fca8d8887382606897d4a97e55062fda Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:43:15 +0200 Subject: [PATCH 0024/3747] Footnotes are 100% in width --- docs/_themes/flasky/static/flasky.css_t | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index 15495a06..7046e24f 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -250,6 +250,7 @@ table.field-list, table.footnote { } table.footnote { + width: 100%; border: 1px solid #eee; -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; From 6a3a046f55755f0691568431e68a8bfea8d89df0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 03:53:38 +0200 Subject: [PATCH 0025/3747] Another doc improvement --- docs/quickstart.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index de8ce039..38e37f26 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -534,7 +534,11 @@ you want to customize the error page, you can use the @app.errorhandler(404) def page_not_found(error): - return render_template('page_not_found.html') + return render_template('page_not_found.html'), 404 + +Note the ``404`` after the :func:`~flask.render_template` call. This +tells Flask that the status code of that page should be 404 which means +not found. By default 200 is assumed which translats to: all went well. .. _sessions: From 0b8e01b4450d0ba8cb65751d866568fddc155fa4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 04:00:17 +0200 Subject: [PATCH 0026/3747] Linked escape --- docs/quickstart.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 38e37f26..91ac4255 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -583,6 +583,9 @@ sessions work:: # set the secret key. keep this really secret: app.secret_key = 'the secret key' +The here mentioned :func:`~flask.escape` does escaping for you if you are +not using the template engine (like in this example). + Message Flashing ---------------- From 03168a5d53457fdb8676a4ac9fca0f76a23c6852 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 17:58:45 +0200 Subject: [PATCH 0027/3747] Removed the useless apishowcase example --- examples/apishowcase/apishowcase.py | 30 ---------------- examples/apishowcase/static/style.css | 7 ---- examples/apishowcase/templates/counter.html | 12 ------- examples/apishowcase/templates/hello.html | 13 ------- examples/apishowcase/templates/index.html | 11 ------ examples/apishowcase/templates/layout.html | 8 ----- examples/minitwit/minitwit.py | 38 ++++++++++++++++----- 7 files changed, 30 insertions(+), 89 deletions(-) delete mode 100644 examples/apishowcase/apishowcase.py delete mode 100644 examples/apishowcase/static/style.css delete mode 100644 examples/apishowcase/templates/counter.html delete mode 100644 examples/apishowcase/templates/hello.html delete mode 100644 examples/apishowcase/templates/index.html delete mode 100644 examples/apishowcase/templates/layout.html diff --git a/examples/apishowcase/apishowcase.py b/examples/apishowcase/apishowcase.py deleted file mode 100644 index b3ab2dd1..00000000 --- a/examples/apishowcase/apishowcase.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import Flask, abort, redirect, request, session, \ - render_template, url_for - -#: create a new flask applications. We pass it the name of our module -#: so that flask knows where to look for templates and static files. -app = Flask(__name__) - - -@app.route('/', methods=['GET']) -def index(): - """Show an overview page""" - return render_template('index.html') - - -@app.route('/hello/', methods=['GET', 'POST']) -def hello_user(): - """Ask the user for a name and redirect to :func:`hello`""" - if request.method == 'POST': - return redirect(url_for('hello', name=request.form['name'])) - return render_template('hello.html', name=None) - - -@app.route('/hello/', methods=['GET']) -def hello(name): - """Greet name friendly""" - return render_template('hello.html', name=name) - - -if __name__ == '__main__': - app.run(debug=True) diff --git a/examples/apishowcase/static/style.css b/examples/apishowcase/static/style.css deleted file mode 100644 index 65a9ec14..00000000 --- a/examples/apishowcase/static/style.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - font-family: 'Trebuchet MS', sans-serif; -} - -a { - color: #44AD80; -} diff --git a/examples/apishowcase/templates/counter.html b/examples/apishowcase/templates/counter.html deleted file mode 100644 index ef888995..00000000 --- a/examples/apishowcase/templates/counter.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

- This is an example application that shows how - the Werkzeug powered Flask microframework works. -

- The various parts of the example application: -

-{% endblock %} diff --git a/examples/apishowcase/templates/hello.html b/examples/apishowcase/templates/hello.html deleted file mode 100644 index dc86737c..00000000 --- a/examples/apishowcase/templates/hello.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "layout.html" %} -{% block body %} - {% if name %} -

Hello {{ name }}!

- {% else %} -

Hello Stranger …

-
-

… What's your name? -

- -

- {% endif %} -{% endblock %} diff --git a/examples/apishowcase/templates/index.html b/examples/apishowcase/templates/index.html deleted file mode 100644 index d210d54f..00000000 --- a/examples/apishowcase/templates/index.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

- This is an example application that shows how - the Werkzeug powered Flask microframework works. -

- The various parts of the example application: -

-{% endblock %} diff --git a/examples/apishowcase/templates/layout.html b/examples/apishowcase/templates/layout.html deleted file mode 100644 index 166d39ec..00000000 --- a/examples/apishowcase/templates/layout.html +++ /dev/null @@ -1,8 +0,0 @@ - -Flask API Showcase - -

Flask API Showcase

-{% if request.endpoint != 'index' %} - -{% endif %} -{% block body %}{% endblock %} diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 06168746..f3bbe7c3 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -1,4 +1,13 @@ # -*- coding: utf-8 -*- +""" + MiniTwit + ~~~~~~~~ + + A microblogging application written with Flask and sqlite3. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" from __future__ import with_statement import re import time @@ -66,21 +75,26 @@ def before_request(): up the current user so that we know he's there. """ g.db = connect_db() + g.user = None if 'user_id' in session: g.user = query_db('select * from user where user_id = ?', [session['user_id']], one=True) @app.request_shutdown -def after_request(request): +def after_request(response): """Closes the database again at the end of the request.""" g.db.close() - return request + return response @app.route('/') def timeline(): - if not 'user_id' in session: + """Shows a users timeline or if no user is logged in it will + redirect to the public timeline. This timeline shows the user's + messages as well as all the messages of followed users. + """ + if not g.user: return redirect(url_for('public_timeline')) offset = request.args.get('offset', type=int) return render_template('timeline.html', messages=query_db(''' @@ -95,6 +109,7 @@ def timeline(): @app.route('/public') def public_timeline(): + """Displays the latest messages of all users.""" return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where message.author_id = user.user_id @@ -103,12 +118,13 @@ def public_timeline(): @app.route('/') def user_timeline(username): + """Display's a users tweets.""" profile_user = query_db('select * from user where username = ?', [username], one=True) if profile_user is None: abort(404) followd = False - if 'user_id' in session: + if g.user: followed = query_db('''select 1 from follower where follower.who_id = ? and follower.whom_id = ?''', [session['user_id'], profile_user['user_id']], one=True) is not None @@ -122,7 +138,8 @@ def user_timeline(username): @app.route('//follow') def follow_user(username): - if not 'user_id' in session: + """Adds the current user as follower of the given user""" + if not g.user: abort(401) whom_id = get_user_id(username) if whom_id is None: @@ -136,7 +153,8 @@ def follow_user(username): @app.route('//unfollow') def unfollow_user(username): - if not 'user_id' in session: + """Removes the current user as follower of the given user""" + if not g.user: abort(401) whom_id = get_user_id(username) if whom_id is None: @@ -150,6 +168,7 @@ def unfollow_user(username): @app.route('/add_message', methods=['POST']) def add_message(): + """Registers a new message for the user""" if 'user_id' not in session: abort(401) if request.form['text']: @@ -163,7 +182,8 @@ def add_message(): @app.route('/login', methods=['GET', 'POST']) def login(): - if 'user_id' in session: + """Logs the user in""" + if g.user: return redirect(url_for('timeline')) error = None if request.method == 'POST': @@ -183,7 +203,8 @@ def login(): @app.route('/register', methods=['GET', 'POST']) def register(): - if 'user_id' in session: + """Registers the user""" + if g.user: return redirect(url_for('timeline')) error = None if request.method == 'POST': @@ -211,6 +232,7 @@ def register(): @app.route('/logout') def logout(): + """Logs the user out""" flash('You were logged out') session.pop('user_id', None) return redirect(url_for('public_timeline')) From f2dc38cda61f76c64b97ab9f730accc986a4b188 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 18:45:06 +0200 Subject: [PATCH 0028/3747] Added tests for minitwit. Testing with Flask is awesome --- examples/minitwit/README | 5 + examples/minitwit/minitwit.py | 2 +- examples/minitwit/minitwit_tests.py | 145 ++++++++++++++++++++++++++++ flask.py | 5 +- 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 examples/minitwit/minitwit_tests.py diff --git a/examples/minitwit/README b/examples/minitwit/README index f054fd8f..e47c8792 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -19,3 +19,8 @@ 3. now you can run the minitwit.py file with your python interpreter and the application will greet you on http://localhost:5000/ + + ~ Is it tested? + + You betcha. Run the `minitwit_tests.py` file to + see the tests pass. diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index f3bbe7c3..05f0689d 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -216,7 +216,7 @@ def register(): elif not request.form['password']: error = 'You have to enter a password' elif request.form['password'] != request.form['password2']: - error = 'The two passwords to not match' + error = 'The two passwords do not match' elif get_user_id(request.form['username']) is not None: error = 'The username is already taken' else: diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py new file mode 100644 index 00000000..a3502f64 --- /dev/null +++ b/examples/minitwit/minitwit_tests.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +""" + MiniTwit Tests + ~~~~~~~~~~~~~~ + + Tests the MiniTwit application. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import minitwit +import unittest +import tempfile +from contextlib import closing + + +class MiniTwitTestCase(unittest.TestCase): + + def setUp(self): + """Before each test, set up a blank database""" + self.db = tempfile.NamedTemporaryFile() + self.app = minitwit.app.test_client() + minitwit.DATABASE = self.db.name + minitwit.init_db() + + # helper functions + + def register(self, username, password, password2=None, email=None): + """Helper function to register a user""" + if password2 is None: + password2 = password + if email is None: + email = username + '@example.com' + return self.app.post('/register', data={ + 'username': username, + 'password': password, + 'password2': password2, + 'email': email, + }, follow_redirects=True) + + def login(self, username, password): + """Helper function to login""" + return self.app.post('/login', data={ + 'username': username, + 'password': password + }, follow_redirects=True) + + def register_and_login(self, username, password): + """Registers and logs in in one go""" + self.register(username, password) + return self.login(username, password) + + def logout(self): + """Helper function to logout""" + return self.app.get('/logout', follow_redirects=True) + + def add_message(self, text): + """Records a message""" + rv = self.app.post('/add_message', data={'text': text}, + follow_redirects=True) + if text: + assert 'Your message was recorded' in rv.data + return rv + + # testing functions + + def test_register(self): + """Make sure registering works""" + rv = self.register('user1', 'default') + assert 'You were successfully registered ' \ + 'and can login now' in rv.data + rv = self.register('user1', 'default') + assert 'The username is already taken' in rv.data + rv = self.register('', 'default') + assert 'You have to enter a username' in rv.data + rv = self.register('meh', '') + assert 'You have to enter a password' in rv.data + rv = self.register('meh', 'x', 'y') + assert 'The two passwords do not match' in rv.data + rv = self.register('meh', 'foo', email='broken') + assert 'You have to enter a valid email address' in rv.data + + def test_login_logout(self): + """Make sure logging in and logging out works""" + rv = self.register_and_login('user1', 'default') + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login('user1', 'wrongpassword') + assert 'Invalid password' in rv.data + rv = self.login('user2', 'wrongpassword') + assert 'Invalid username' in rv.data + + def test_message_recording(self): + """Check if adding messages works""" + self.register_and_login('foo', 'default') + self.add_message('test message 1') + self.add_message('') + rv = self.app.get('/') + assert 'test message 1' in rv.data + assert '<test message 2>' in rv.data + + def test_timelines(self): + """Make sure that timelines work""" + self.register_and_login('foo', 'default') + self.add_message('the message by foo') + self.logout() + self.register_and_login('bar', 'default') + self.add_message('the message by bar') + rv = self.app.get('/public') + assert 'the message by foo' in rv.data + assert 'the message by bar' in rv.data + + # bar's timeline should just show bar's message + rv = self.app.get('/') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + + # now let's follow foo + rv = self.app.get('/foo/follow', follow_redirects=True) + assert 'You are now following "foo"' in rv.data + + # we should now see foo's message + rv = self.app.get('/') + assert 'the message by foo' in rv.data + assert 'the message by bar' in rv.data + + # but on the user's page we only want the user's message + rv = self.app.get('/bar') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + rv = self.app.get('/foo') + assert 'the message by foo' in rv.data + assert 'the message by bar' not in rv.data + + # now unfollow and check if that worked + rv = self.app.get('/foo/unfollow', follow_redirects=True) + assert 'You are no longer following "foo"' in rv.data + rv = self.app.get('/') + assert 'the message by foo' not in rv.data + assert 'the message by bar' in rv.data + + +if __name__ == '__main__': + unittest.main() diff --git a/flask.py b/flask.py index 1ea239f8..21858f5e 100644 --- a/flask.py +++ b/flask.py @@ -265,9 +265,8 @@ class Flask(object): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) - @cached_property - def test(self): - """A test client for this application""" + def test_client(self): + """Creates a test client for this application""" from werkzeug import Client return Client(self, self.response_class, use_cookies=True) From ef524f2cfd65377b3b9ce896e7a710472b13c0f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 18:47:32 +0200 Subject: [PATCH 0029/3747] No need for the closing import --- examples/minitwit/minitwit_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py index a3502f64..cd761aa6 100644 --- a/examples/minitwit/minitwit_tests.py +++ b/examples/minitwit/minitwit_tests.py @@ -11,7 +11,6 @@ import minitwit import unittest import tempfile -from contextlib import closing class MiniTwitTestCase(unittest.TestCase): From 03148dba6b26a2694da1d466584f50189c52e7b3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 11 Apr 2010 19:18:40 +0200 Subject: [PATCH 0030/3747] Added testing documentation --- docs/api.rst | 16 ++++++++++++++++ docs/index.rst | 1 + flask.py | 36 ++++++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 98614ead..e3439393 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -19,6 +19,8 @@ Application Object Incoming Request Data --------------------- +.. autoclass:: Request + .. class:: request To access incoming request data, you can use the global `request` @@ -104,6 +106,20 @@ Incoming Request Data `root_url` ``http://www.example.com/myapplication/`` ============= ====================================================== +Response Objects +---------------- + +.. autoclass:: flask.Response + :members: set_cookie, data, mimetype + + .. attribute:: headers + + A :class:`Headers` object representing the response headers. + + .. attribute:: status_code + + The response status as integer. + Sessions -------- diff --git a/docs/index.rst b/docs/index.rst index 81528203..5c622fe6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,4 +21,5 @@ you want to dive into all the internal parts of Flask, check out the patterns api deploying + testing becomingbig diff --git a/flask.py b/flask.py index 21858f5e..0c3a8b32 100644 --- a/flask.py +++ b/flask.py @@ -15,8 +15,8 @@ import pkg_resources from threading import local from contextlib import contextmanager from jinja2 import Environment, PackageLoader -from werkzeug import Request, Response, LocalStack, LocalProxy, \ - create_environ, cached_property +from werkzeug import Request as RequestBase, Response as ResponseBase, \ + LocalStack, LocalProxy, create_environ, cached_property from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie @@ -27,21 +27,29 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape -class FlaskRequest(Request): +class Request(RequestBase): """The request object used by default in flask. Remembers the matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. """ def __init__(self, environ): - Request.__init__(self, environ) + RequestBase.__init__(self, environ) self.endpoint = None self.view_args = None -class FlaskResponse(Response): +class Response(ResponseBase): """The response object that is used by default in flask. Works like the response object from Werkzeug but is set to have a HTML mimetype by - default. + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.request_class` to your subclass. """ default_mimetype = 'text/html' @@ -142,11 +150,13 @@ class Flask(object): app = Flask(__name__) """ - #: the class that is used for request objects - request_class = FlaskRequest + #: the class that is used for request objects. See :class:`~flask.request` + #: for more information. + request_class = Request - #: the class that is used for response objects - response_class = FlaskResponse + #: the class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response #: path for the static files. If you don't want to use static files #: you can set this value to `None` in which case no URL rule is added @@ -266,7 +276,9 @@ class Flask(object): return run_simple(host, port, self, **options) def test_client(self): - """Creates a test client for this application""" + """Creates a test client for this application. For information + about unit testing head over to :ref:`testing`. + """ from werkzeug import Client return Client(self, self.response_class, use_cookies=True) @@ -356,7 +368,7 @@ class Flask(object): Variables parts in the route can be specified with angular brackets (``/user/``). By default a variable part - in the URL accepts any string without a slash however a differnt + in the URL accepts any string without a slash however a different converter can be specified as well by using ````. Variable parts are passed to the view function as keyword From 2f5a4f8dbc832b0daebcd66ea8b3969589191fa7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Apr 2010 00:14:59 +0200 Subject: [PATCH 0031/3747] Doc updates and typo fixes --- docs/testing.rst | 116 ++++++++++++++++++++++ examples/minitwit/README | 2 +- examples/minitwit/minitwit.py | 18 ++-- examples/minitwit/templates/timeline.html | 2 +- 4 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 docs/testing.rst diff --git a/docs/testing.rst b/docs/testing.rst new file mode 100644 index 00000000..4c04414d --- /dev/null +++ b/docs/testing.rst @@ -0,0 +1,116 @@ +.. _testing: + +Testing Flask Applications +========================== + + **Something that is untested is broken.** + +Not sure where that is coming from, and it's not entirely correct, but +also not that far from the truth. Untested applications make it hard to +improve existing code and developers of untested applications tend to +become pretty paranoid. If an application however has automated tests you +can savely change things and you will instantly know if your change broke +something. + +Flask gives you a couple of ways to test applications. It mainly does +that by exposing the Werkzeug test :class:`~werkzeug.Client` class to your +code and handling the context locals for you. You can then use that with +your favourite testing solution. In this documentation we will us the +:mod:`unittest` package that comes preinstalled with each Python +installation. + +The Application +--------------- + +First we need an application to test for functionality. Let's start +simple with a Hello World application (`hello.py`):: + + from flask import Flask, render_template_string + app = Flask(__name__) + + @app.route('/') + @app.route('/') + def hello(name='World'): + return render_template_string(''' + + Hello {{ name }}! +

Hello {{ name }}!

+ ''', name=name) + +The Testing Skeleton +-------------------- + +In order to test that, we add a second module ( +`hello_tests.py`) and create a unittest skeleton there:: + + import unittest + import hello + + class HelloWorldTestCase(unittest.TestCase): + + def setUp(self): + self.app = hello.app.test_client() + + if __name__ == '__main__': + unittest.main() + +The code in the `setUp` function creates a new test client. That function +is called before each individual test function. What the test client does +for us is giving us a simple interface to the application. We can trigger +test requests to the application and the client will also keep track of +cookies for us. + +If we now run that testsuite, we should see the following output:: + + $ python hello_tests.py + + ---------------------------------------------------------------------- + Ran 0 tests in 0.000s + + OK + +Even though it did not run any tests, we already know that our hello +application is syntactically valid, otherwise the import would have died +with an exception. + +The First Test +-------------- + +Now we can add the first test. Let's check that the application greets us +with "Hello World" if we access it on ``/``. For that we modify our +created test case class so that it looks like this:: + + class HelloWorldTestCase(unittest.TestCase): + + def setUp(self): + self.app = hello.app.test_client() + + def test_hello_world(self): + rv = self.app.get('/') + assert 'Hello World!' in rv.data + +Test functions begin with the word `test`. Every function named like that +will be picked up automatically. By using `self.app.get` we can send an +HTTP `GET` request to the application with the given path. The return +value will be a :class:`~flask.Flask.response_class` object. We can now +use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the +return value (as string) from the application. In this case, we ensure +that ``'Hello World!'`` is part of the output. + +Run it again and you should see one passing test. Let's add a second test +here:: + + def test_hello_name(self): + rv = self.app.get('/Peter') + assert 'Hello Peter!' in rv.data + +Of course you can submit forms with the test client as well. For that and +other features of the test client, check the documentation of the Werkzeug +test :class:`~werkzeug.Client` and the tests of the MiniTwit example +application: + +- Werkzeug Test :class:`~werkzeug.Client` +- `MiniTwit Example`_ + +.. _MiniTwit Example: + http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ diff --git a/examples/minitwit/README b/examples/minitwit/README index e47c8792..065674a9 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -10,7 +10,7 @@ ~ How do I use it? - 1. edit the configurtion in the minitwit.py file + 1. edit the configuration in the minitwit.py file 2. fire up a python shell and run this: diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 05f0689d..37e6cb5c 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -31,7 +31,7 @@ app = Flask(__name__) def connect_db(): - """Returns a new database connection to the database.""" + """Returns a new connection to the database.""" return sqlite3.connect(DATABASE) @@ -52,19 +52,19 @@ def query_db(query, args=(), one=False): def get_user_id(username): - """Convenience method to look up the id for a username""" + """Convenience method to look up the id for a username.""" rv = g.db.execute('select user_id from user where username = ?', [username]).fetchone() return rv[0] if rv else None def format_datetime(timestamp): - """Format a timestamp for display""" + """Format a timestamp for display.""" return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') def gravatar_url(email, size=80): - """Return the gravatar image for the given email address""" + """Return the gravatar image for the given email address.""" return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) @@ -138,7 +138,7 @@ def user_timeline(username): @app.route('//follow') def follow_user(username): - """Adds the current user as follower of the given user""" + """Adds the current user as follower of the given user.""" if not g.user: abort(401) whom_id = get_user_id(username) @@ -153,7 +153,7 @@ def follow_user(username): @app.route('//unfollow') def unfollow_user(username): - """Removes the current user as follower of the given user""" + """Removes the current user as follower of the given user.""" if not g.user: abort(401) whom_id = get_user_id(username) @@ -168,7 +168,7 @@ def unfollow_user(username): @app.route('/add_message', methods=['POST']) def add_message(): - """Registers a new message for the user""" + """Registers a new message for the user.""" if 'user_id' not in session: abort(401) if request.form['text']: @@ -182,7 +182,7 @@ def add_message(): @app.route('/login', methods=['GET', 'POST']) def login(): - """Logs the user in""" + """Logs the user in.""" if g.user: return redirect(url_for('timeline')) error = None @@ -203,7 +203,7 @@ def login(): @app.route('/register', methods=['GET', 'POST']) def register(): - """Registers the user""" + """Registers the user.""" if g.user: return redirect(url_for('timeline')) error = None diff --git a/examples/minitwit/templates/timeline.html b/examples/minitwit/templates/timeline.html index 892b8fcc..ea7d751b 100644 --- a/examples/minitwit/templates/timeline.html +++ b/examples/minitwit/templates/timeline.html @@ -43,7 +43,7 @@ {{ message.text }} — {{ message.pub_date|datetimeformat }} {% else %} -
  • There are no messages so far. +
  • There's no message so far. {% endfor %} {% endblock %} From 51c9829a9cdfa2bf301431f686d7a404cc7c5083 Mon Sep 17 00:00:00 2001 From: moraes Date: Mon, 12 Apr 2010 08:15:35 +0800 Subject: [PATCH 0032/3747] Fixed couple of errors in docs. --- docs/foreword.rst | 2 +- docs/index.rst | 2 +- docs/installation.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 6b40921f..deeff8ca 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -9,7 +9,7 @@ What does Micro Mean? --------------------- The micro in microframework for me means on the one hand being small in -size, complexity but on the other hand also that the complexity of the +size and complexity but on the other hand also that the complexity of the applications that are written with these frameworks do not exceed a certain size. A microframework like Flask sacrifices a few things in order to be approachable and to be as concise as possible. diff --git a/docs/index.rst b/docs/index.rst index 5c622fe6..cb7abf96 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ Welcome to Flask :alt: The Flask Logo with Subtitle :align: right -Welcome to Flask's documentation. This documentation is divided into +Welcome to Flask's documentation. This documentation is divided in different parts. I would suggest to get started with the :ref:`installation` and then heading over to the :ref:`quickstart`. If you want to dive into all the internal parts of Flask, check out the diff --git a/docs/installation.rst b/docs/installation.rst index 89e5a680..f83d9c28 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,7 +14,7 @@ templates. Now you are maybe asking, what is WSGI? WSGI is a standard in Python that is basically responsible for ensuring that your application is behaving in a specific way that you can run it on different environments (for example on a local development server, on an Apache2, on -lighttpd, on Google's appengine or whatever you have in mind). +lighttpd, on Google's App Engine or whatever you have in mind). So how do you get all that on your computer in no time? The most kick-ass method is virtualenv, so let's look at that first. @@ -93,7 +93,7 @@ This is possible as well, but I would not recommend it. Just run sudo easy_install Flask -(Run it in an Admin shell on Windows systems and without the `sudo`). +(Run it in an Admin shell on Windows systems and without the `sudo`). The Drop into Place Version From 36071ca1f42a26ef327e9d14e4909a8c92cd4fae Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Mon, 12 Apr 2010 22:55:48 +0800 Subject: [PATCH 0033/3747] added in example tornado and gevent usage for flask integration --- docs/deploying.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/deploying.rst b/docs/deploying.rst index ea812c17..7045e51e 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -223,6 +223,44 @@ For more information consult the `mod_wsgi wiki`_. .. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ + +Tornado +-------- + +`Tornado`_ is an open source version of the scalable, non-blocking web server and tools that power `FriendFeed`_. +Because it is non-blocking and uses epoll, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. +Integrating this service with Flask is a trivial task:: + + + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from yourapplication import app + + http_server = HTTPServer(WSGIContainer(app)) + http_server.listen(5000) + IOLoop.instance().start() + + +.. _Tornado: http://www.tornadoweb.org/ +.. _FriendFeed: http://friendfeed.com/ + + +Gevent +------- + +`Gevent`_ is a coroutine-based Python networking library that uses `greenlet`_ to provide a high-level synchronous API on top of `libevent`_ event loop:: + + from gevent.wsgi import WSGIServer + from yourapplication import app + + http_server = WSGIServer(('', 5000), app) + http_server.serve_forever() + +.. _Gevent: http://www.gevent.org/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html +.. _libevent: http://monkey.org/~provos/libevent/ + CGI --- @@ -265,3 +303,4 @@ In Apache for example you can put a like like this into the config: ScriptName /app /path/to/the/application.cgi For more information consult the documentation of your webserver. + From a01e8b49ca66608f0bd46134ff02deeb5724c799 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Apr 2010 19:52:18 +0200 Subject: [PATCH 0034/3747] Fixed a documentation error and implemented template context processors. --- flask.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/flask.py b/flask.py index 0c3a8b32..b45c49a4 100644 --- a/flask.py +++ b/flask.py @@ -130,6 +130,18 @@ def render_template_string(source, **context): return current_app.jinja_env.from_string(source).render(context) +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) + + class Flask(object): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -216,6 +228,14 @@ class Flask(object): #: To register a function here use the :meth:`request_shtdown` #: decorator. self.request_shutdown_funcs = [] + + #: a list of functions that are called without arguments + #: to populate the template context. Each returns a dictionary + #: that the template context is updated with. + #: To register a function here, use the :meth:`context_processor` + #: decorator. + self.template_context_processors = [_default_template_ctx_processor] + self.url_map = Map() if self.static_path is not None: @@ -248,9 +268,8 @@ class Flask(object): to add extra variables. """ reqctx = _request_ctx_stack.top - context['request'] = reqctx.request - context['session'] = reqctx.session - context['g'] = reqctx.g + for func in self.template_context_processors: + context.update(func()) def run(self, host='localhost', port=5000, **options): """Runs the application on a local development server. If the @@ -325,7 +344,7 @@ class Flask(object): :param session: the session to be saved (a :class:`~werkzeug.contrib.securecookie.SecureCookie` object) - :param request: an instance of :attr:`response_class` + :param response: an instance of :attr:`response_class` """ if session is not None: session.save_cookie(response, self.session_cookie_name) @@ -462,6 +481,11 @@ class Flask(object): self.request_shutdown_funcs.append(f) return f + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors.append(f) + return f + def match_request(self): """Matches the current request against the URL map and also stores the endpoint and view arguments on the request object From de03143f6ecf9a36dd32f16f07309c64d4de03b0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Apr 2010 20:19:16 +0200 Subject: [PATCH 0035/3747] Added some basic tests for flask itself --- flask_tests.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 flask_tests.py diff --git a/flask_tests.py b/flask_tests.py new file mode 100644 index 00000000..8308e19e --- /dev/null +++ b/flask_tests.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" + Flask Tests + ~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import flask +import unittest +import tempfile + + +class ContextTestCase(unittest.TestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + assert index() == 'Hello World!' + with app.test_request_context('/meh'): + assert meh() == 'http://localhost/meh' + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + assert c.get('/').data == 'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD'] + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == 'POST' + assert c.get('/more').data == 'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + assert c.post('/set', data={'value': '42'}).data == 'value set' + assert c.get('/get').data == '42' + + +if __name__ == '__main__': + unittest.main() From 4affedec356ad8c9e6a107578f03fc7f5c615064 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Apr 2010 20:39:52 +0200 Subject: [PATCH 0036/3747] Testing URL generation --- flask_tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flask_tests.py b/flask_tests.py index 8308e19e..298f39b8 100644 --- a/flask_tests.py +++ b/flask_tests.py @@ -69,6 +69,14 @@ class ContextTestCase(unittest.TestCase): assert c.post('/set', data={'value': '42'}).data == 'value set' assert c.get('/get').data == '42' + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + assert flask.url_for('hello', name='test x') == '/hello/test%20x' + if __name__ == '__main__': unittest.main() From 959b5df00431c436474c5abca304e631c0de7721 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Apr 2010 01:08:52 +0200 Subject: [PATCH 0037/3747] Flask can depend on Jinja2 and Werkzeug now that they are relased. --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a0bddf4e..e5f160ad 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,7 @@ setup( zip_safe=False, platforms='any', install_requires=[ - ## disabled until release, install yourself - # 'Werkzeug', - # 'Jinja2' + 'Werkzeug>=0.6.1', + 'Jinja2>=2.4' ] ) From a42728568e18bedf588be2357b56021ab85b9fad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Apr 2010 01:42:31 +0200 Subject: [PATCH 0038/3747] Improved Flask README regarding Jinja2 and Werkzeug --- README | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README b/README index f3a8a03e..dc70692d 100644 --- a/README +++ b/README @@ -18,10 +18,10 @@ ~ What do I need? - Currently the development versions of Jinja2 and Werkzeug. - Because of Flask I did some refactoring in Werkzeug and - Jinja2 and the next versions (out April 10th) will contain - these changes. + Jinja 2.4 and Werkzeug 0.6.1. `easy_install` will + install them for you if you do `easy_install Flask==dev`. + I encourage you to use a virtualenv. Check the docs for + complete installation and usage instructions. ~ Where are the docs? From 8009bd986940378bba1399dbfce90d03e441467b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 02:17:55 +0200 Subject: [PATCH 0039/3747] Fixed the mod_wsgi docs. Thanks Graham --- docs/deploying.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying.rst b/docs/deploying.rst index 7045e51e..e04112f0 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -204,7 +204,7 @@ application under a different user for security reasons: ServerName example.com - WSGIDaemonProcess yourapplication user=user1 group=group1 processes=1 threads=5 + WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi From 08f1f0dc32004fae01cf96d1eed84e6461e698e5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 02:42:12 +0200 Subject: [PATCH 0040/3747] Added more Flask tests --- Makefile | 11 ++ flask_tests.py | 82 ----------- tests/flask_tests.py | 179 +++++++++++++++++++++++++ tests/templates/context_template.html | 1 + tests/templates/escaping_template.html | 6 + 5 files changed, 197 insertions(+), 82 deletions(-) create mode 100644 Makefile delete mode 100644 flask_tests.py create mode 100644 tests/flask_tests.py create mode 100644 tests/templates/context_template.html create mode 100644 tests/templates/escaping_template.html diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0eabe5d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: clean-pyc test + +all: clean-pyc test + +test: + python tests/flask_tests.py + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + diff --git a/flask_tests.py b/flask_tests.py deleted file mode 100644 index 298f39b8..00000000 --- a/flask_tests.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flask Tests - ~~~~~~~~~~~ - - Tests Flask itself. The majority of Flask is already tested - as part of Werkzeug. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import with_statement -import flask -import unittest -import tempfile - - -class ContextTestCase(unittest.TestCase): - - def test_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - @app.route('/meh') - def meh(): - return flask.request.url - - with app.test_request_context('/?name=World'): - assert index() == 'Hello World!' - with app.test_request_context('/meh'): - assert meh() == 'http://localhost/meh' - - def test_request_dispatching(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.request.method - @app.route('/more', methods=['GET', 'POST']) - def more(): - return flask.request.method - - c = app.test_client() - assert c.get('/').data == 'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] - - def test_session(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - @app.route('/set', methods=['POST']) - def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') - def get(): - return flask.session['value'] - - c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == 'value set' - assert c.get('/get').data == '42' - - def test_url_generation(self): - app = flask.Flask(__name__) - @app.route('/hello/', methods=['POST']) - def hello(): - pass - with app.test_request_context(): - assert flask.url_for('hello', name='test x') == '/hello/test%20x' - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/flask_tests.py b/tests/flask_tests.py new file mode 100644 index 00000000..515a25c3 --- /dev/null +++ b/tests/flask_tests.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +""" + Flask Tests + ~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import flask +import unittest +import tempfile + + +class ContextTestCase(unittest.TestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + assert index() == 'Hello World!' + with app.test_request_context('/meh'): + assert meh() == 'http://localhost/meh' + + +class BasicFunctionality(unittest.TestCase): + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + assert c.get('/').data == 'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD'] + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == 'POST' + assert c.get('/more').data == 'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + assert c.post('/set', data={'value': '42'}).data == 'value set' + assert c.get('/get').data == '42' + + def test_request_processing(self): + app = flask.Flask(__name__) + evts = [] + @app.request_init + def before_request(): + evts.append('before') + @app.request_shutdown + def after_request(response): + response.data += '|after' + evts.append('after') + return response + @app.route('/') + def index(): + assert 'before' in evts + assert 'after' not in evts + return 'request' + assert 'after' not in evts + rv = app.test_client().get('/').data + assert 'after' in evts + assert rv == 'request|after' + + def test_error_handling(self): + app = flask.Flask(__name__) + @app.errorhandler(404) + def not_found(e): + return 'not found', 404 + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @app.route('/') + def index(): + flask.abort(404) + @app.route('/error') + def error(): + 1/0 + c = app.test_client() + rv = c.get('/') + assert rv.status_code == 404 + assert rv.data == 'not found' + rv = c.get('/error') + assert rv.status_code == 500 + assert 'internal server error' in rv.data + + def test_response_creation(self): + app = flask.Flask(__name__) + @app.route('/unicode') + def from_unicode(): + return u'Hällo Wörld' + @app.route('/string') + def from_string(): + return u'Hällo Wörld'.encode('utf-8') + @app.route('/args') + def from_tuple(): + return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + c = app.test_client() + assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') + assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') + rv = c.get('/args') + assert rv.data == 'Meh' + assert rv.headers['X-Foo'] == 'Testing' + assert rv.status_code == 400 + assert rv.mimetype == 'text/plain' + + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + assert flask.url_for('hello', name='test x') == '/hello/test%20x' + + +class Templating(unittest.TestCase): + + def test_context_processing(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'injected_value': 42} + @app.route('/') + def index(): + return flask.render_template('context_template.html', value=23) + rv = app.test_client().get('/') + assert rv.data == '

    23|42' + + def test_escaping(self): + text = '

    Hello World!' + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('escaping_template.html', text=text, + html=flask.Markup(text)) + lines = app.test_client().get('/').data.splitlines() + assert lines == [ + '<p>Hello World!', + '

    Hello World!', + '

    Hello World!', + '

    Hello World!', + '<p>Hello World!', + '

    Hello World!' + ] + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/templates/context_template.html b/tests/templates/context_template.html new file mode 100644 index 00000000..fadf3e5d --- /dev/null +++ b/tests/templates/context_template.html @@ -0,0 +1 @@ +

    {{ value }}|{{ injected_value }} diff --git a/tests/templates/escaping_template.html b/tests/templates/escaping_template.html new file mode 100644 index 00000000..dc47644d --- /dev/null +++ b/tests/templates/escaping_template.html @@ -0,0 +1,6 @@ +{{ text }} +{{ html }} +{% autoescape false %}{{ text }} +{{ html }}{% endautoescape %} +{% autoescape true %}{{ text }} +{{ html }}{% endautoescape %} From ca520fb7e4c29afccc637e61c11429a0f3e8d5ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 14:11:00 +0200 Subject: [PATCH 0041/3747] Static files are active in the WSGI app now, not just the server. --- flask.py | 10 +++++----- tests/flask_tests.py | 9 +++++++++ tests/static/index.html | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 tests/static/index.html diff --git a/flask.py b/flask.py index b45c49a4..68e25ade 100644 --- a/flask.py +++ b/flask.py @@ -16,7 +16,8 @@ from threading import local from contextlib import contextmanager from jinja2 import Environment, PackageLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, cached_property + LocalStack, LocalProxy, create_environ, cached_property, \ + SharedDataMiddleware from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie @@ -241,6 +242,9 @@ class Flask(object): if self.static_path is not None: self.url_map.add(Rule(self.static_path + '/', build_only=True, endpoint='static')) + self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { + self.static_path: (self.package_name, 'static') + }) #: the Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned @@ -286,10 +290,6 @@ class Flask(object): from werkzeug import run_simple if 'debug' in options: self.debug = options.pop('debug') - if self.static_path is not None: - options['static_files'] = { - self.static_path: (self.package_name, 'static') - } options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 515a25c3..dec05bea 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -143,6 +143,15 @@ class BasicFunctionality(unittest.TestCase): with app.test_request_context(): assert flask.url_for('hello', name='test x') == '/hello/test%20x' + def test_static_files(self): + app = flask.Flask(__name__) + rv = app.test_client().get('/static/index.html') + assert rv.status_code == 200 + assert rv.data.strip() == '

    Hello World!

    ' + with app.test_request_context(): + assert flask.url_for('static', filename='index.html') \ + == '/static/index.html' + class Templating(unittest.TestCase): diff --git a/tests/static/index.html b/tests/static/index.html new file mode 100644 index 00000000..de8b69b6 --- /dev/null +++ b/tests/static/index.html @@ -0,0 +1 @@ +

    Hello World!

    From 2d9bb692728fd76b19e467499caa4f5fcb07aede Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 15:16:38 +0200 Subject: [PATCH 0042/3747] Flask now runs without the presence of pkg_resources as well. --- flask.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/flask.py b/flask.py index 68e25ade..08376993 100644 --- a/flask.py +++ b/flask.py @@ -11,10 +11,10 @@ """ import os import sys -import pkg_resources + from threading import local from contextlib import contextmanager -from jinja2 import Environment, PackageLoader +from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, cached_property, \ SharedDataMiddleware @@ -27,6 +27,15 @@ from werkzeug.contrib.securecookie import SecureCookie from werkzeug import abort, redirect from jinja2 import Markup, escape +# use pkg_resource if that works, otherwise fall back to cwd. The +# current working directory is generally not reliable with the notable +# exception of google appengine. +try: + import pkg_resources + pkg_resources.resource_stream +except (ImportError, AttributeError): + pkg_resources = None + class Request(RequestBase): """The request object used by default in flask. Remembers the @@ -202,6 +211,10 @@ class Flask(object): #: it was set by the constructor. self.package_name = package_name + #: where is the app root located? + self.root_path = os.path.abspath(os.path.dirname( + sys.modules[self.package_name].__file__)) + #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. @@ -242,8 +255,12 @@ class Flask(object): if self.static_path is not None: self.url_map.add(Rule(self.static_path + '/', build_only=True, endpoint='static')) + if pkg_resources is not None: + target = (self.package_name, 'static') + else: + target = os.path.join(self.root_path, 'static') self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { - self.static_path: (self.package_name, 'static') + self.static_path: target }) #: the Jinja2 environment. It is created from the @@ -262,6 +279,8 @@ class Flask(object): `templates` folder. To add other loaders it's possible to override this method. """ + if pkg_resources is None: + return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.package_name) def update_template_context(self, context): @@ -323,6 +342,8 @@ class Flask(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ + if pkg_resources is None: + return open(os.path.join(self.root_path, resource), 'rb') return pkg_resources.resource_stream(self.package_name, resource) def open_session(self, request): From c33675f0251071ea47ce166b81ce8c637842f091 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 16:44:29 +0200 Subject: [PATCH 0043/3747] Added mini blogging application as Flask example. This should become the tutorial. --- examples/flaskr/flaskr.py | 103 ++++++++++++++++++++ examples/flaskr/flaskr_tests.py | 64 ++++++++++++ examples/flaskr/schema.sql | 6 ++ examples/flaskr/static/style.css | 16 +++ examples/flaskr/templates/layout.html | 17 ++++ examples/flaskr/templates/login.html | 14 +++ examples/flaskr/templates/show_entries.html | 21 ++++ 7 files changed, 241 insertions(+) create mode 100644 examples/flaskr/flaskr.py create mode 100644 examples/flaskr/flaskr_tests.py create mode 100644 examples/flaskr/schema.sql create mode 100644 examples/flaskr/static/style.css create mode 100644 examples/flaskr/templates/layout.html create mode 100644 examples/flaskr/templates/login.html create mode 100644 examples/flaskr/templates/show_entries.html diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py new file mode 100644 index 00000000..10a6ff8b --- /dev/null +++ b/examples/flaskr/flaskr.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + Flaskr + ~~~~~~ + + A microblog example application written as Flask tutorial with + Flask and sqlite3. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import time +import sqlite3 +from contextlib import closing +from flask import Flask, request, session, g, redirect, url_for, abort, \ + render_template, flash +from werkzeug import secure_filename + +# configuration +DATABASE = '/tmp/flaskr.db' +DEBUG = True +SECRET_KEY = 'development key' +USERNAME = 'admin' +PASSWORD = 'default' + +# create our little application :) +app = Flask(__name__) +app.secret_key = SECRET_KEY +app.debug = DEBUG + + +def connect_db(): + """Returns a new connection to the database.""" + return sqlite3.connect(DATABASE) + + +def init_db(): + """Creates the database tables.""" + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + + +@app.request_init +def before_request(): + """Make sure we are connected to the database each request. Also + set `g.logged_in` to `True` if we are logged in. + """ + g.db = connect_db() + g.logged_in = session.get('logged_in', False) + + +@app.request_shutdown +def after_request(response): + """Closes the database again at the end of the request.""" + g.db.close() + return response + + +@app.route('/') +def show_entries(): + cur = g.db.execute('select title, text from entries order by id desc') + entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + return render_template('show_entries.html', entries=entries) + + +@app.route('/add', methods=['POST']) +def add_entry(): + if not g.logged_in: + abort(401) + g.db.execute('insert into entries (title, text) values (?, ?)', + [request.form['title'], request.form['text']]) + g.db.commit() + flash('New entry was successfully posted') + return redirect(url_for('show_entries')) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + error = None + if request.method == 'POST': + if request.form['username'] != USERNAME: + error = 'Invalid username' + elif request.form['password'] != PASSWORD: + error = 'Invalid password' + else: + session['logged_in'] = True + flash('You were logged in') + return redirect(url_for('show_entries')) + return render_template('login.html', error=error) + + +@app.route('/logout') +def logout(): + session.pop('logged_in', None) + flash('You were logged out') + return redirect(url_for('show_entries')) + + +if __name__ == '__main__': + app.run() diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py new file mode 100644 index 00000000..f8a05976 --- /dev/null +++ b/examples/flaskr/flaskr_tests.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" + Flaskr Tests + ~~~~~~~~~~~~ + + Tests the Flaskr application. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flaskr +import unittest +import tempfile + + +class FlaskrTestCase(unittest.TestCase): + + def setUp(self): + """Before each test, set up a blank database""" + self.db = tempfile.NamedTemporaryFile() + self.app = flaskr.app.test_client() + flaskr.DATABASE = self.db.name + flaskr.init_db() + + def login(self, username, password): + return self.app.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) + + def logout(self): + return self.app.get('/logout', follow_redirects=True) + + # testing functions + + def test_login_logout(self): + """Make sure login and logout works""" + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) + assert 'Invalid username' in rv.data + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') + assert 'Invalid password' in rv.data + + def test_messages(self): + """Test that messages work""" + # start with a blank state + rv = self.app.get('/') + assert 'No entries here so far' in rv.data + self.login(flaskr.USERNAME, flaskr.PASSWORD) + rv = self.app.post('/add', data=dict( + title='', + text='HTML allowed here' + ), follow_redirects=True) + assert 'No entries here so far' not in rv.data + self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert '<Hello>' in rv.data + assert 'HTML allowed here' in rv.data + + +if __name__ == '__main__': + unittest.main() diff --git a/examples/flaskr/schema.sql b/examples/flaskr/schema.sql new file mode 100644 index 00000000..970cca77 --- /dev/null +++ b/examples/flaskr/schema.sql @@ -0,0 +1,6 @@ +drop table if exists entries; +create table entries ( + id integer primary key autoincrement, + title string not null, + text string not null +); diff --git a/examples/flaskr/static/style.css b/examples/flaskr/static/style.css new file mode 100644 index 00000000..b0f38774 --- /dev/null +++ b/examples/flaskr/static/style.css @@ -0,0 +1,16 @@ +body { font-family: sans-serif; background: #eee; } +a, h1, h2 { color: #377BA8; } +h1, h2 { font-family: 'Georgia', serif; margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; } +div.metanav { text-align: right; font-size: 0.8em; background: #fafafa; + padding: 0.3em; margin-bottom: 1em; } +ul.entries { list-style: none; margin: 0; padding: 0; } +ul.entries li { margin: 0.8em 1.2em; } +ul.entries li h2 { margin-left: -1em; } +div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +form.add-entry dl { font-weight: bold; } +div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } +p.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/templates/layout.html b/examples/flaskr/templates/layout.html new file mode 100644 index 00000000..eafcefa9 --- /dev/null +++ b/examples/flaskr/templates/layout.html @@ -0,0 +1,17 @@ + +Flaskr + +
    +

    Flaskr

    +
    + {% if not g.logged_in %} + log in + {% else %} + log out + {% endif %} +
    + {% for message in get_flashed_messages() %} +
    {{ message }}
    + {% endfor %} + {% block body %}{% endblock %} +
    diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/templates/login.html new file mode 100644 index 00000000..568e9e84 --- /dev/null +++ b/examples/flaskr/templates/login.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block body %} +

    Login

    + {% if error %}

    Error: {{ error }}{% endif %} +

    +
    +
    Username: +
    +
    Password: +
    +
    +
    +
    +{% endblock %} diff --git a/examples/flaskr/templates/show_entries.html b/examples/flaskr/templates/show_entries.html new file mode 100644 index 00000000..55940ee7 --- /dev/null +++ b/examples/flaskr/templates/show_entries.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block body %} + {% if g.logged_in %} +
    +
    +
    Title: +
    +
    Text: +
    +
    +
    +
    + {% endif %} +
      + {% for entry in entries %} +
    • {{ entry.title }}

      {{ entry.text|safe }} + {% else %} +
    • Unbelievable. No entries here so far + {% endfor %} +
    +{% endblock %} From fc94599285aa1aec50b3c9426697249e55edda0a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 16:47:53 +0200 Subject: [PATCH 0044/3747] Removed useless imports and added README --- examples/flaskr/README | 26 ++++++++++++++++++++++++++ examples/flaskr/flaskr.py | 2 -- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 examples/flaskr/README diff --git a/examples/flaskr/README b/examples/flaskr/README new file mode 100644 index 00000000..ae1bb51c --- /dev/null +++ b/examples/flaskr/README @@ -0,0 +1,26 @@ + + / Flaskr / + + a minimal blog application + + + ~ What is Flask? + + A sqlite powered thumble blog application + + ~ How do I use it? + + 1. edit the configuration in the flaskr.py file + + 2. fire up a python shell and run this: + + >>> from flaskr import init_db; init_db() + + 3. now you can run the flaskr.py file with your + python interpreter and the application will + greet you on http://localhost:5000/ + + ~ Is it tested? + + You betcha. Run the `flaskr_tests.py` file to see + the tests pass. diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 10a6ff8b..02db8afd 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -10,12 +10,10 @@ :license: BSD, see LICENSE for more details. """ from __future__ import with_statement -import time import sqlite3 from contextlib import closing from flask import Flask, request, session, g, redirect, url_for, abort, \ render_template, flash -from werkzeug import secure_filename # configuration DATABASE = '/tmp/flaskr.db' From 1d8432ebfd1637eacab79a15e9b7963bf061371c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 17:08:38 +0200 Subject: [PATCH 0045/3747] Can't believe I forgot that future import. --- flask.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask.py b/flask.py index 08376993..eaafa310 100644 --- a/flask.py +++ b/flask.py @@ -9,6 +9,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement import os import sys From c4f5c2fb9afc1679c085500830bd53fab93ad885 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Apr 2010 17:09:34 +0200 Subject: [PATCH 0046/3747] Fixed a typo --- examples/flaskr/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flaskr/README b/examples/flaskr/README index ae1bb51c..4a9a02c6 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -4,7 +4,7 @@ a minimal blog application - ~ What is Flask? + ~ What is Flaskr? A sqlite powered thumble blog application From 1246f4088a4fdc00a79f8029ff5d77bfe4fc9b14 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Apr 2010 02:21:46 +0200 Subject: [PATCH 0047/3747] First part of the tutorial. Many explanations missing but it's a start. --- docs/_static/flaskr.png | Bin 0 -> 53571 bytes docs/_themes/flasky/static/flasky.css_t | 12 + docs/index.rst | 9 +- docs/quickstart.rst | 13 + docs/tutorial.rst | 354 ++++++++++++++++++++++++ examples/flaskr/flaskr.py | 7 +- examples/flaskr/static/style.css | 9 +- examples/flaskr/templates/layout.html | 2 +- examples/flaskr/templates/login.html | 2 +- 9 files changed, 394 insertions(+), 14 deletions(-) create mode 100644 docs/_static/flaskr.png create mode 100644 docs/tutorial.rst diff --git a/docs/_static/flaskr.png b/docs/_static/flaskr.png new file mode 100644 index 0000000000000000000000000000000000000000..07d027dd973f6ecf02986d98bc8bf541e174c057 GIT binary patch literal 53571 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ÐNKC!{nB=)@lkWiHYwoId6sR_vl!q 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 from flaskr import init_db +>>> init_db() + +The :meth:`~flask.Flask.open_resource` function opens a file from the +resource location (your flaskr folder) and allows you to read from it. We +are using this here to execute a script on the database connection. + +When we connect to a database we get a connection object (here called +`db`) that can give us a cursor. On that cursor there is a method to +execute a complete script. Finally we only have to commit the changes and +close the transaction. + +Step 4: Request Database Connections +------------------------------------ + +Now we know how we can open database connections and use them for scripts, +but how can we elegantly do that for requests? We will need the database +connection in all our functions so it makes sense to initialize them +before each request and shut them down afterwards. + +Flask allows us to do that with the :meth:`~flask.Flask.request_init` and +:meth:`~flask.Flask.request_shutdown` decorators:: + + @app.request_init + def before_request(): + g.db = connect_db() + + @app.request_shutdown + def after_request(response): + g.db.close() + return response + +Functions marked with :meth:`~flask.Flask.request_init` are called before +a request and passed no arguments, functions marked with +:meth:`~flask.Flask.request_shutdown` are called after a request and +passed the response that will be sent to the client. They have to return +that response object or a different one. In this case we just return it +unchanged. + +We store our current database connection on the special :data:`~flask.g` +object that flask provides for us. This object stores information for one +request only and is available from within each function. Never store such +things on other objects because this would not work with threaded +environments. That special :data:`~flask.g` object does some magic behind +the scenes to ensure it does the right thing. + +Step 5: The View Functions +-------------------------- + +Now that the database connections are working we can start writing the +view functions. We will need for of them: + +Show Entries +```````````` + +This view shows all the entries stored in the database:: + + @app.route('/') + def show_entries(): + cur = g.db.execute('select title, text from entries order by id desc') + entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + return render_template('show_entries.html', entries=entries) + +Add New Entry +````````````` + +This view lets the user add new entries if he's logged in. This only +responds to `POST` requests, the actual form is shown on the +`show_entries` page:: + + @app.route('/add', methods=['POST']) + def add_entry(): + if not session.get('logged_in'): + abort(401) + g.db.execute('insert into entries (title, text) values (?, ?)', + [request.form['title'], request.form['text']]) + g.db.commit() + flash('New entry was successfully posted') + return redirect(url_for('show_entries')) + +Login and Logout +```````````````` + +These functions are used to sign the user in and out:: + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != USERNAME: + error = 'Invalid username' + elif request.form['password'] != PASSWORD: + error = 'Invalid password' + else: + session['logged_in'] = True + flash('You were logged in') + return redirect(url_for('show_entries')) + return render_template('login.html', error=error) + + @app.route('/logout') + def logout(): + session.pop('logged_in', None) + flash('You were logged out') + return redirect(url_for('show_entries')) + +Step 6: The Templates +--------------------- + +Now we should start working on the templates. If we request the URLs now +we would only get an exception that Flask cannot find the templates. + +Put the following templates into the `templates` folder: + +layout.html +``````````` + +.. sourcecode:: html+jinja + + + Flaskr + +
    +

    Flaskr

    +
    + {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} +
    + {% for message in get_flashed_messages() %} +
    {{ message }}
    + {% endfor %} + {% block body %}{% endblock %} +
    + +show_entries.html +````````````````` + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} + {% if g.logged_in %} +
    +
    +
    Title: +
    +
    Text: +
    +
    +
    +
    + {% endif %} +
      + {% for entry in entries %} +
    • {{ entry.title }}

      {{ entry.text|safe }} + {% else %} +
    • Unbelievable. No entries here so far + {% endfor %} +
    + {% endblock %} + +login.html +`````````` + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

    Login

    + {% if error %}

    Error: {{ error }}{% endif %} +

    +
    +
    Username: +
    +
    Password: +
    +
    +
    +
    + {% endblock %} diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 02db8afd..1158e158 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -43,11 +43,8 @@ def init_db(): @app.request_init def before_request(): - """Make sure we are connected to the database each request. Also - set `g.logged_in` to `True` if we are logged in. - """ + """Make sure we are connected to the database each request.""" g.db = connect_db() - g.logged_in = session.get('logged_in', False) @app.request_shutdown @@ -66,7 +63,7 @@ def show_entries(): @app.route('/add', methods=['POST']) def add_entry(): - if not g.logged_in: + if not session.get('logged_in'): abort(401) g.db.execute('insert into entries (title, text) values (?, ?)', [request.form['title'], request.form['text']]) diff --git a/examples/flaskr/static/style.css b/examples/flaskr/static/style.css index b0f38774..39e0a8e7 100644 --- a/examples/flaskr/static/style.css +++ b/examples/flaskr/static/style.css @@ -3,14 +3,15 @@ a, h1, h2 { color: #377BA8; } h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } -div.metanav { text-align: right; font-size: 0.8em; background: #fafafa; - padding: 0.3em; margin-bottom: 1em; } + +div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } ul.entries { list-style: none; margin: 0; padding: 0; } ul.entries li { margin: 0.8em 1.2em; } ul.entries li h2 { margin-left: -1em; } -div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } form.add-entry dl { font-weight: bold; } +div.metanav { text-align: right; font-size: 0.8em; background: #fafafa; + padding: 0.3em; margin-bottom: 1em; } div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } p.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/templates/layout.html b/examples/flaskr/templates/layout.html index eafcefa9..cbdb9650 100644 --- a/examples/flaskr/templates/layout.html +++ b/examples/flaskr/templates/layout.html @@ -4,7 +4,7 @@

    Flaskr

    - {% if not g.logged_in %} + {% if not session.logged_in %} log in {% else %} log out diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/templates/login.html index 568e9e84..6f70bb76 100644 --- a/examples/flaskr/templates/login.html +++ b/examples/flaskr/templates/login.html @@ -7,7 +7,7 @@
    Username:
    Password: -
    +
    From 6dd92ae4b32c336564231e10db12458a8b9261ca Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 02:03:45 +0200 Subject: [PATCH 0048/3747] Beefed up the tutorial --- docs/patterns.rst | 2 + docs/quickstart.rst | 8 + docs/testing.rst | 157 +++++++++++----- docs/tutorial.rst | 188 +++++++++++++++++--- examples/flaskr/flaskr_tests.py | 8 +- examples/flaskr/static/style.css | 33 ++-- examples/flaskr/templates/show_entries.html | 2 +- 7 files changed, 306 insertions(+), 92 deletions(-) diff --git a/docs/patterns.rst b/docs/patterns.rst index c7b4769a..acb6788f 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -40,6 +40,8 @@ So here a simple example how you can use SQLite 3 with Flask:: g.db.close() return response +.. _easy-querying: + Easy Querying ````````````` diff --git a/docs/quickstart.rst b/docs/quickstart.rst index df476867..1733b301 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -89,6 +89,14 @@ Or pass it to run:: Both will have exactly the same effect. +.. admonition:: Attention + + The interactive debugger however does not work in forking environments + which makes it nearly impossible to use on production servers but the + debugger still allows the execution of arbitrary code which makes it a + major security risk and **must never be used on production machines** + because of that. + Routing ------- diff --git a/docs/testing.rst b/docs/testing.rst index 4c04414d..62b309ce 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -22,72 +22,78 @@ installation. The Application --------------- -First we need an application to test for functionality. Let's start -simple with a Hello World application (`hello.py`):: +First we need an application to test for functionality. For the testing +we will use the application from the :ref:`tutorial`. If you don't have +that application yet, get the sources from `the examples`_. - from flask import Flask, render_template_string - app = Flask(__name__) - - @app.route('/') - @app.route('/') - def hello(name='World'): - return render_template_string(''' - - Hello {{ name }}! -

    Hello {{ name }}!

    - ''', name=name) +.. _the examples: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ The Testing Skeleton -------------------- In order to test that, we add a second module ( -`hello_tests.py`) and create a unittest skeleton there:: +`flaskr_tests.py`) and create a unittest skeleton there:: import unittest - import hello + import flaskr + import tempfile - class HelloWorldTestCase(unittest.TestCase): + class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.app = hello.app.test_client() + self.db = tempfile.NamedTemporaryFile() + self.app = flaskr.app.test_client() + flaskr.DATABASE = self.db.name + flaskr.init_db() if __name__ == '__main__': unittest.main() -The code in the `setUp` function creates a new test client. That function -is called before each individual test function. What the test client does -for us is giving us a simple interface to the application. We can trigger -test requests to the application and the client will also keep track of -cookies for us. +The code in the `setUp` function creates a new test client and initialize +a new database. That function is called before each individual test function. +What the test client does for us is giving us a simple interface to the +application. We can trigger test requests to the application and the +client will also keep track of cookies for us. + +Because SQLite3 is filesystem based we can easily use the tempfile module +to create a temporary database and initialize it. Just make sure that you +keep a reference to the :class:`~tempfile.NamedTemporaryFile` around (we +store it as `self.db` because of that) so that the garbage collector does +not remove that object and with it the database from the filesystem. If we now run that testsuite, we should see the following output:: - $ python hello_tests.py + $ python flaskr_tests.py ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK -Even though it did not run any tests, we already know that our hello +Even though it did not run any tests, we already know that our flaskr application is syntactically valid, otherwise the import would have died with an exception. The First Test -------------- -Now we can add the first test. Let's check that the application greets us -with "Hello World" if we access it on ``/``. For that we modify our -created test case class so that it looks like this:: +Now we can add the first test. Let's check that the application shows +"No entries here so far" if we access the root of the application (``/``). +For that we modify our created test case class so that it looks like +this:: - class HelloWorldTestCase(unittest.TestCase): + class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.app = hello.app.test_client() + self.db = tempfile.NamedTemporaryFile() + self.app = flaskr.app.test_client() + flaskr.DATABASE = self.db.name + flaskr.init_db() - def test_hello_world(self): + def test_empty_db(self): rv = self.app.get('/') - assert 'Hello World!' in rv.data + assert 'No entries here so far' in rv.data Test functions begin with the word `test`. Every function named like that will be picked up automatically. By using `self.app.get` we can send an @@ -95,22 +101,87 @@ HTTP `GET` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure -that ``'Hello World!'`` is part of the output. +that ``'No entries here so far'`` is part of the output. -Run it again and you should see one passing test. Let's add a second test -here:: +Run it again and you should see one passing test:: - def test_hello_name(self): - rv = self.app.get('/Peter') - assert 'Hello Peter!' in rv.data + $ python flaskr_tests.py + . + ---------------------------------------------------------------------- + Ran 1 test in 0.034s -Of course you can submit forms with the test client as well. For that and -other features of the test client, check the documentation of the Werkzeug -test :class:`~werkzeug.Client` and the tests of the MiniTwit example -application: + OK + +Of course you can submit forms with the test client as well which we will +use now to log our user in. + +Logging In and Out +------------------ + +The majority of the functionality of our application is only available for +the administration user. So we need a way to log our test client into the +application and out of it again. For that we fire some requests to the +login and logout pages with the required form data (username and +password). Because the login and logout pages redirect, we tell the +client to `follow_redirects`. + +Add the following two methods do your `FlaskrTestCase` class:: + + def login(self, username, password): + return self.app.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) + + def logout(self): + return self.app.get('/logout', follow_redirects=True) + +Now we can easily test if logging in and out works and that it fails with +invalid credentials. Add this as new test to the class:: + + def test_login_logout(self): + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) + assert 'Invalid username' in rv.data + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') + assert 'Invalid password' in rv.data + +Test Adding Messages +-------------------- + +Now we can also test that adding messages works. Add a new test method +like this:: + + def test_messages(self): + self.login(flaskr.USERNAME, flaskr.PASSWORD) + rv = self.app.post('/add', data=dict( + title='', + text='HTML allowed here' + ), follow_redirects=True) + assert 'No entries here so far' not in rv.data + self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert '<Hello>' in rv.data + assert 'HTML allowed here' in rv.data + +Here we also check that HTML is allowed in the text but not in the title +which is the intended behavior. + +Running that should now give us three passing tests:: + + $ python flaskr_tests.py + ... + ---------------------------------------------------------------------- + Ran 3 tests in 0.332s + + OK + +For more complex tests with headers and status codes, check out the +`MiniTwit Example`_ from the sources. That one contains a larger test +suite. -- Werkzeug Test :class:`~werkzeug.Client` -- `MiniTwit Example`_ .. _MiniTwit Example: http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ diff --git a/docs/tutorial.rst b/docs/tutorial.rst index bbf9fa6c..bdd624c0 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -31,6 +31,13 @@ less web-2.0-ish name ;) Basically we want it to do the following things: 3. the page shows all entries so far in reverse order (newest on top) and the user can add new ones from there if logged in. +We will be using SQlite3 directly for that application because it's good +enough for an application of that size. For larger applications however +it makes a lot of sense to use `SQLAlchemy`_ that handles database +connections in a more intelligent way, allows you to target different +relational databases at once and more. You might also want to consider +one of the popular NoSQL databases if your data is more suited for those. + Here a screenshot from the final application: .. image:: _static/flaskr.png @@ -38,6 +45,8 @@ Here a screenshot from the final application: :class: screenshot :alt: screenshot of the final application +.. _SQLAlchemy: http://www.sqlalchemy.org/ + Step 0: Creating The Folders ---------------------------- @@ -50,7 +59,13 @@ application:: The `flaskr` folder is not a python package, but just something where we drop our files. Directly into this folder we will then put our database -schema as well as main module in the following steps. +schema as well as main module in the following steps. The files inside +the `static` folder are available to users of the application via `HTTP`. +This is the place where css and javascript files go. Inside the +`templates` folder Flask will look for `Jinja2`_ templates. Drop all the +templates there. + +.. _Jinja2: http://jinja.pocoo.org/2/ Step 1: Database Schema ----------------------- @@ -79,12 +94,18 @@ Step 2: Application Setup Code Now that we have the schema in place we can create the application module. Let's call it `flaskr.py` inside the `flaskr` folder. For starters we -will add the imports we will need as well as the config section:: +will add the imports we will need as well as the config section. For +small applications it's a possibility to drop the configuration directly +into the module which we will be doing here. However a cleaner solution +would be to create a separate `.ini` or `.py` file and load that or import +the values from there. + +:: # all the imports import sqlite3 - from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash + from flask import Flask, request, session, g, redirect, url_for, \ + abort, render_template, flash # configuration DATABASE = '/tmp/flaskr.db' @@ -93,17 +114,25 @@ will add the imports we will need as well as the config section:: USERNAME = 'admin' PASSWORD = 'default' -The `with_statement` and :func:`~contextlib.closing` function are used to -make dealing with the database connection easier later on for setting up -the initial database. Next we can create our actual application and -initialize it with the config:: +Next we can create our actual application and initialize it with the +config:: # create our little application :) app = Flask(__name__) app.secret_key = SECRET_KEY app.debug = DEBUG -We can also add a method to easily connect to the database sepcified:: +The `secret_key` is needed to keep the client-side sessions secure. +Choose that key wisely and as hard to guess and complex as possible. The +debug flag enables or disables the interactive debugger. Never leave +debug mode activated in a production system because it will allow users to +executed code on the server! + +We also add a method to easily connect to the database specified. That +can be used to open a connection on request and also from the interactive +Python shell or a script. This will come in handy later + +:: def connect_db(): return sqlite3.connect(DATABASE) @@ -114,6 +143,11 @@ server if we run that file as standalone application:: if __name__ == '__main__': app.run() +With that out of the way you should be able to start up the application +without problems. When you head over to the server you will get an 404 +page not found error because we don't have any views yet. But we will +focus on that a little later. First we should get the database working. + .. admonition:: Troubleshooting If you notice later that the browser cannot connect to the server @@ -125,11 +159,6 @@ server if we run that file as standalone application:: default and not every browser is happy with that. This forces IPv4 usage. -With that out of the way you should be able to start up the application -without problems. When you head over to the server you will get an 404 -page not found error because we don't have any views yet. But we will -focus on that a little later. First we should get the database working. - Step 3: Creating The Database ----------------------------- @@ -159,7 +188,8 @@ first (`__future__` imports must be the very first import):: from contextlib import closing Next we can create a function called `init_db` that initializes the -database:: +database. For this we can use the `connect_db` function we defined +earlier. Just add that function below the `connect_db` function:: def init_db(): with closing(connect_db()) as db: @@ -167,21 +197,26 @@ database:: db.cursor().executescript(f.read()) db.commit() +The :func:`~contextlib.closing` helper function allows us to keep a +connection open for the duration of the `with` block. The +:func:`~flask.Flask.open_resource` method of the application object +supports that functionality out of the box, so it can be used in the +`with` block directly. This function opens a file from the resource +location (your `flaskr` folder) and allows you to read from it. We are +using this here to execute a script on the database connection. + +When we connect to a database we get a connection object (here called +`db`) that can give us a cursor. On that cursor there is a method to +execute a complete script. Finally we only have to commit the changes. +SQLite 3 and other transactional databases will not commit unless you +explicitly tell it to. + Now it is possible to create a database by starting up a Python shell and importing and calling that function:: >>> from flaskr import init_db >>> init_db() -The :meth:`~flask.Flask.open_resource` function opens a file from the -resource location (your flaskr folder) and allows you to read from it. We -are using this here to execute a script on the database connection. - -When we connect to a database we get a connection object (here called -`db`) that can give us a cursor. On that cursor there is a method to -execute a complete script. Finally we only have to commit the changes and -close the transaction. - Step 4: Request Database Connections ------------------------------------ @@ -225,7 +260,16 @@ view functions. We will need for of them: Show Entries ```````````` -This view shows all the entries stored in the database:: +This view shows all the entries stored in the database. It listens on the +root of the application and will select title and text from the database. +The one with the highest id (the newest entry) on top. The rows returned +from the cursor are tuples with the columns ordered like specified in the +select statement. This is good enough for small applications like here, +but you might want to convert them into a dict. If you are interested how +to do that, check out the :ref:`easy-querying` example. + +The view function will pass the entries as dicts to the +`show_entries.html` template and return the rendered one:: @app.route('/') def show_entries(): @@ -238,7 +282,9 @@ Add New Entry This view lets the user add new entries if he's logged in. This only responds to `POST` requests, the actual form is shown on the -`show_entries` page:: +`show_entries` page. If everything worked out well we will +:func:`~flask.flash` an information message to the next request and +redirect back to the `show_entries` page:: @app.route('/add', methods=['POST']) def add_entry(): @@ -250,10 +296,19 @@ responds to `POST` requests, the actual form is shown on the flash('New entry was successfully posted') return redirect(url_for('show_entries')) +Note that we check that the user is logged in here (the `logged_in` key is +present in the session and `True`). + Login and Logout ```````````````` -These functions are used to sign the user in and out:: +These functions are used to sign the user in and out. Login checks the +username and password against the ones from the configuration and sets the +`logged_in` key in the session. If the user logged in successfully that +key is set to `True` and the user is redirected back to the `show_entries` +page. In that case also a message is flashed that informs the user he or +she was logged in successfully. If an error occoured the template is +notified about that and the user asked again:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -269,6 +324,15 @@ These functions are used to sign the user in and out:: return redirect(url_for('show_entries')) return render_template('login.html', error=error) +The logout function on the other hand removes that key from the session +again. We use a neat trick here: if you use the :meth:`~dict.pop` method +of the dict and pass a second parameter to it (the default) the method +will delete the key from the dictionary if present or do nothing when that +key was not in there. This is helpful because we don't have to check in +that case if the user was logged in. + +:: + @app.route('/logout') def logout(): session.pop('logged_in', None) @@ -279,13 +343,32 @@ Step 6: The Templates --------------------- Now we should start working on the templates. If we request the URLs now -we would only get an exception that Flask cannot find the templates. +we would only get an exception that Flask cannot find the templates. The +templates are using `Jinja2`_ syntax and have autoescaping enabled by +default. This means that unless you mark a value in the code with +:class:`~flask.Markup` or with the ``|safe`` filter in the template, +Jinja2 will ensure that special characters such as ``<`` or ``>`` are +escaped with their XML equivalents. + +We are also using template inheritance which makes it possible to reuse +the layout of the website in all pages. Put the following templates into the `templates` folder: layout.html ``````````` +This template contains the HTML skeleton, the header and a link to log in +(or log out if the user was already logged in). It also displays the +flashed messages if there are any. The ``{% block body %}`` block can be +replaced by a block of the same name (``body``) in a child template. + +The :class:`~flask.session` dict is available in the template as well and +you can use that to check if the user is logged in or not. Note that in +Jinja you can access missing attributes and items of objects / dicts which +makes the following code work, even if there is no ``'logged_in'`` key in +the session: + .. sourcecode:: html+jinja @@ -309,11 +392,17 @@ layout.html show_entries.html ````````````````` +This template extends the `layout.html` template from above to display the +messages. Note that the `for` loop iterates over the messages we passed +in with the :func:`~flask.render_template` function. We also tell the +form to submit to your `add_entry` function and use `POST` as `HTTP` +method: + .. sourcecode:: html+jinja {% extends "layout.html" %} {% block body %} - {% if g.logged_in %} + {% if session.logged_in %}
    Title: @@ -336,6 +425,9 @@ show_entries.html login.html `````````` +Finally the login template which basically just displays a form to allow +the user to login: + .. sourcecode:: html+jinja {% extends "layout.html" %} @@ -352,3 +444,41 @@ login.html
    {% endblock %} + +Step 7: Adding Style +-------------------- + +Now that everything else works, it's time to add some style to the +application. Just create a stylesheet called `style.css` in the `static` +folder we created before: + +.. sourcecode:: css + + body { font-family: sans-serif; background: #eee; } + a, h1, h2 { color: #377BA8; } + h1, h2 { font-family: 'Georgia', serif; margin: 0; } + h1 { border-bottom: 2px solid #eee; } + h2 { font-size: 1.2em; } + + .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } + .entries { list-style: none; margin: 0; padding: 0; } + .entries li { margin: 0.8em 1.2em; } + .entries li h2 { margin-left: -1em; } + .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } + .add-entry dl { font-weight: bold; } + .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } + .flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } + .error { background: #F0D6D6; padding: 0.5em; } + +Bonus: Testing the Application +------------------------------- + +Now that you have finished the application and everything works as +expected, it's probably not the best idea to add automated tests to +simplify modifications in the future. The application above is used as a +basic example of how to perform unittesting in the :ref:`testing` section +of the documentation. Go there to see how easy it is to test Flask +applications. diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index f8a05976..e8f01437 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -33,6 +33,11 @@ class FlaskrTestCase(unittest.TestCase): # testing functions + def test_empty_db(self): + """Start with a blank database.""" + rv = self.app.get('/') + assert 'No entries here so far' in rv.data + def test_login_logout(self): """Make sure login and logout works""" rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) @@ -46,9 +51,6 @@ class FlaskrTestCase(unittest.TestCase): def test_messages(self): """Test that messages work""" - # start with a blank state - rv = self.app.get('/') - assert 'No entries here so far' in rv.data self.login(flaskr.USERNAME, flaskr.PASSWORD) rv = self.app.post('/add', data=dict( title='', diff --git a/examples/flaskr/static/style.css b/examples/flaskr/static/style.css index 39e0a8e7..4f3b71d8 100644 --- a/examples/flaskr/static/style.css +++ b/examples/flaskr/static/style.css @@ -1,17 +1,18 @@ -body { font-family: sans-serif; background: #eee; } -a, h1, h2 { color: #377BA8; } -h1, h2 { font-family: 'Georgia', serif; margin: 0; } -h1 { border-bottom: 2px solid #eee; } -h2 { font-size: 1.2em; } +body { font-family: sans-serif; background: #eee; } +a, h1, h2 { color: #377BA8; } +h1, h2 { font-family: 'Georgia', serif; margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; } -div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } -ul.entries { list-style: none; margin: 0; padding: 0; } -ul.entries li { margin: 0.8em 1.2em; } -ul.entries li h2 { margin-left: -1em; } -form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } -form.add-entry dl { font-weight: bold; } -div.metanav { text-align: right; font-size: 0.8em; background: #fafafa; - padding: 0.3em; margin-bottom: 1em; } -div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } -p.error { background: #F0D6D6; padding: 0.5em; } +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +.entries { list-style: none; margin: 0; padding: 0; } +.entries li { margin: 0.8em 1.2em; } +.entries li h2 { margin-left: -1em; } +.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +.add-entry dl { font-weight: bold; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } +.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/templates/show_entries.html b/examples/flaskr/templates/show_entries.html index 55940ee7..fabe65ec 100644 --- a/examples/flaskr/templates/show_entries.html +++ b/examples/flaskr/templates/show_entries.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} {% block body %} - {% if g.logged_in %} + {% if session.logged_in %}
    Title: From 40e0024d7b87150ed694829a5335bd2435962225 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 02:14:54 +0200 Subject: [PATCH 0049/3747] Added screenshot of the debugger to Flask docs. Flask now runs from the shell again. --- docs/_static/debugger.png | Bin 0 -> 123545 bytes docs/quickstart.rst | 7 +++++++ flask.py | 11 +++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 docs/_static/debugger.png diff --git a/docs/_static/debugger.png b/docs/_static/debugger.png new file mode 100644 index 0000000000000000000000000000000000000000..4f47229d6a24d5e42980c274591b1de30d7ff0d5 GIT binary patch literal 123545 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 z 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 z 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 Date: Fri, 16 Apr 2010 02:27:37 +0200 Subject: [PATCH 0050/3747] Preserve the request context in debug mode. This makes it possible to access request information in the interactive debugger. Closes #8. --- flask.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/flask.py b/flask.py index 30a0977f..ad62e947 100644 --- a/flask.py +++ b/flask.py @@ -14,7 +14,6 @@ import os import sys from threading import local -from contextlib import contextmanager from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, cached_property, \ @@ -84,6 +83,16 @@ class _RequestContext(object): self.g = _RequestGlobals() self.flashes = None + def __enter__(self): + _request_ctx_stack.push(self) + + def __exit__(self, exc_type, exc_value, tb): + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. + if tb is None or not self.app.debug: + _request_ctx_stack.pop() + def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. @@ -618,7 +627,6 @@ class Flask(object): response = self.process_response(response) return response(environ, start_response) - @contextmanager def request_context(self, environ): """Creates a request context from the given environment and binds it to the current context. This must be used in combination with @@ -632,11 +640,7 @@ class Flask(object): :params environ: a WSGI environment """ - _request_ctx_stack.push(_RequestContext(self, environ)) - try: - yield - finally: - _request_ctx_stack.pop() + return _RequestContext(self, environ) def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see From fb2d2e446bdd806ea3de7b869c7371e2dae57a23 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 11:03:16 +0200 Subject: [PATCH 0051/3747] request_init -> before_request and request_shutdown -> after_request This fixes #9. --- docs/patterns.rst | 8 ++++---- docs/tutorial.rst | 14 +++++++------- examples/flaskr/flaskr.py | 4 ++-- examples/minitwit/minitwit.py | 4 ++-- flask.py | 25 +++++++++++++------------ tests/flask_tests.py | 4 ++-- 6 files changed, 30 insertions(+), 29 deletions(-) diff --git a/docs/patterns.rst b/docs/patterns.rst index acb6788f..d6bfe2ef 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -11,8 +11,8 @@ request and get the information of the currently logged in user. At the end of the request, the database connection is closed again. In Flask you can implement such things with the -:meth:`~flask.Flask.request_init` and -:meth:`~flask.Flask.request_shutdown` decorators in combination with the +:meth:`~flask.Flask.before_request` and +:meth:`~flask.Flask.after_request` decorators in combination with the special :class:`~flask.g` object. @@ -31,11 +31,11 @@ So here a simple example how you can use SQLite 3 with Flask:: def connect_db(): return sqlite3.connect(DATABASE) - @app.request_init + @app.before_request def before_request(): g.db = connect_db() - @app.request_shutdown + @app.after_request def after_request(response): g.db.close() return response diff --git a/docs/tutorial.rst b/docs/tutorial.rst index bdd624c0..f642423f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -225,21 +225,21 @@ but how can we elegantly do that for requests? We will need the database connection in all our functions so it makes sense to initialize them before each request and shut them down afterwards. -Flask allows us to do that with the :meth:`~flask.Flask.request_init` and -:meth:`~flask.Flask.request_shutdown` decorators:: +Flask allows us to do that with the :meth:`~flask.Flask.before_request` and +:meth:`~flask.Flask.after_request` decorators:: - @app.request_init + @app.before_request def before_request(): g.db = connect_db() - @app.request_shutdown + @app.after_request def after_request(response): g.db.close() return response -Functions marked with :meth:`~flask.Flask.request_init` are called before +Functions marked with :meth:`~flask.Flask.before_request` are called before a request and passed no arguments, functions marked with -:meth:`~flask.Flask.request_shutdown` are called after a request and +:meth:`~flask.Flask.after_request` are called after a request and passed the response that will be sent to the client. They have to return that response object or a different one. In this case we just return it unchanged. @@ -255,7 +255,7 @@ Step 5: The View Functions -------------------------- Now that the database connections are working we can start writing the -view functions. We will need for of them: +view functions. We will need four of them: Show Entries ```````````` diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 1158e158..f2a8b341 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -41,13 +41,13 @@ def init_db(): db.commit() -@app.request_init +@app.before_request def before_request(): """Make sure we are connected to the database each request.""" g.db = connect_db() -@app.request_shutdown +@app.after_request def after_request(response): """Closes the database again at the end of the request.""" g.db.close() diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 37e6cb5c..c053e27f 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -69,7 +69,7 @@ def gravatar_url(email, size=80): (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) -@app.request_init +@app.before_request def before_request(): """Make sure we are connected to the database each request and look up the current user so that we know he's there. @@ -81,7 +81,7 @@ def before_request(): [session['user_id']], one=True) -@app.request_shutdown +@app.after_request def after_request(response): """Closes the database again at the end of the request.""" g.db.close() diff --git a/flask.py b/flask.py index ad62e947..f7ba22e6 100644 --- a/flask.py +++ b/flask.py @@ -249,16 +249,16 @@ class Flask(object): #: of the request before request dispatching kicks in. This #: can for example be used to open database connections or #: getting hold of the currently logged in user. - #: To register a function here, use the :meth:`request_init` + #: To register a function here, use the :meth:`before_request` #: decorator. - self.request_init_funcs = [] + self.before_request_funcs = [] #: a list of functions that are called at the end of the #: request. Tha function is passed the current response #: object and modify it in place or replace it. - #: To register a function here use the :meth:`request_shtdown` + #: To register a function here use the :meth:`after_request` #: decorator. - self.request_shutdown_funcs = [] + self.after_request_funcs = [] #: a list of functions that are called without arguments #: to populate the template context. Each returns a dictionary @@ -509,14 +509,14 @@ class Flask(object): return f return decorator - def request_init(self, f): + def before_request(self, f): """Registers a function to run before each request.""" - self.request_init_funcs.append(f) + self.before_request_funcs.append(f) return f - def request_shutdown(self, f): + def after_request(self, f): """Register a function to be run after each request.""" - self.request_shutdown_funcs.append(f) + self.after_request_funcs.append(f) return f def context_processor(self, f): @@ -583,19 +583,20 @@ class Flask(object): def preprocess_request(self): """Called before the actual request dispatching and will - call every as :func:`request_init` decorated function. + call every as :meth:`before_request` decorated function. If any of these function returns a value it's handled as if it was the return value from the view and further request handling is stopped. """ - for func in self.request_init_funcs: + for func in self.before_request_funcs: rv = func() if rv is not None: return rv def process_response(self, response): """Can be overridden in order to modify the response object - before it's sent to the WSGI server. + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. :param response: a :attr:`response_class` object. :return: a new response object or the same, has to be an @@ -604,7 +605,7 @@ class Flask(object): session = _request_ctx_stack.top.session if session is not None: self.save_session(session, response) - for handler in self.request_shutdown_funcs: + for handler in self.after_request_funcs: response = handler(response) return response diff --git a/tests/flask_tests.py b/tests/flask_tests.py index dec05bea..0d73c954 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -75,10 +75,10 @@ class BasicFunctionality(unittest.TestCase): def test_request_processing(self): app = flask.Flask(__name__) evts = [] - @app.request_init + @app.before_request def before_request(): evts.append('before') - @app.request_shutdown + @app.after_request def after_request(response): response.data += '|after' evts.append('after') From 26f86b1d4954dd7512684130ee803b42eca8d72e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 12:21:31 +0200 Subject: [PATCH 0052/3747] Restructured documentation a bit. --- docs/index.rst | 34 ++++++++- docs/patterns.rst | 189 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 214 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 864a3a35..1879070a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,24 @@ you rather want to dive into all the internal parts of Flask, check out the :ref:`api` documentation. Common patterns are described in the :ref:`patterns` section. +Flask also depends on two external libraries: the `Jinja2`_ template +engine and the `Werkzeug`_ WSGI toolkit. both of which are not documented +here. If you want to dive into their documentation check out the +following links: + +- `Jinja2 Documentation `_ +- `Werkzeug Documentation `_ + +.. _Jinja2: http://jinja.pocoo.org/2/ +.. _Werkzeug: http://werkzeug.pocoo.org/ + +Textual Documentation +--------------------- + +This part of the documentation is written text and should give you an idea +how to work with Flask. It's a series of step-by-step instructions for +web development. + .. toctree:: :maxdepth: 2 @@ -21,8 +39,18 @@ the :ref:`api` documentation. Common patterns are described in the installation quickstart tutorial - patterns - api - deploying testing + patterns + deploying becomingbig + +Reference +--------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you: + +.. toctree:: + :maxdepth: 2 + + api diff --git a/docs/patterns.rst b/docs/patterns.rst index d6bfe2ef..91e0d5b6 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -1,7 +1,7 @@ .. _patterns: -Patterns in Flask -================= +Patterns for Flask +================== Certain things are common enough that the changes are high you will find them in most web applications. For example quite a lot of applications @@ -10,10 +10,93 @@ changes are they will open a database connection at the beginning of the request and get the information of the currently logged in user. At the end of the request, the database connection is closed again. -In Flask you can implement such things with the -:meth:`~flask.Flask.before_request` and -:meth:`~flask.Flask.after_request` decorators in combination with the -special :class:`~flask.g` object. + +.. _larger-applications: + +Larger Applications +------------------- + +For larger applications it's a good idea to use a package instead of a +module. That is quite simple. Imagine a small application looks like +this:: + + /yourapplication + /yourapplication.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + +To convert that into a larger one, just create a new folder +`yourapplication` inside the existing one and move everything below it. +Then rename `yourapplication.py` to `__init__.py`. (Make sure to delete +all `.pyc` files first, otherwise things would most likely break) + +You should then end up with something like that:: + + /yourapplication + /yourapplication + /__init__.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + +But how do you run your application now? The naive ``python +yourapplication/__init__.py`` will not work. Let's just say that Python +does not want modules in packages to be the startup file. But that is not +a big problem, just add a new file called `runserver.py` next to the inner +`yourapplication` folder with the following contents:: + + from yourapplication import app + app.run(debug=True) + +What did we gain from this? Now we can restructure the application a bit +into multiple modules. The only thing you have to remember is the +following quick checklist: + +1. the `Flask` application object creation have to be in the + `__init__.py` file. That way each module can import it safely and the + `__name__` variable will resole to the correct package. +2. all the view functions (the ones with a :meth:`~flask.Flask.route` + decorator on top) have to be imported when in the `__init__.py` file. + Not the objects itself, but the module it is in. Do the importing at + the *bottom* of the file. + +Here an example `__init__.py`:: + + from flask import Flask + app = Flask(__name__) + + import yourapplication.views + +And this is what `views.py` would look like:: + + from yourapplication import app + + @app.route('/') + def index(): + return 'Hello World!' + +.. admonition:: Circular Imports + + Every Python programmer hates it, and yet we just did that: circular + imports (That's when one module depends on another one. In this case + `views.py` depends on `__init__.py`). Be advised that this is a bad + idea in general but here it is actually fine. The reason for this is + that we are not actually using the views in `__init__.py` and just + ensuring the module is imported and we are doing that at the bottom of + the file. + + There are still some problems with that approach but if you want to use + decorators there is no way around that. Check out the + :ref:`becomingbig` section for some inspiration how to deal with that. .. _database-pattern: @@ -21,6 +104,11 @@ special :class:`~flask.g` object. Using SQLite 3 with Flask ------------------------- +In Flask you can implement opening of dabase connections at the beginning +of the request and closing at the end with the +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` +decorators in combination with the special :class:`~flask.g` object. + So here a simple example how you can use SQLite 3 with Flask:: import sqlite3 @@ -99,6 +187,95 @@ You can then create such a database from the python shell: >>> from yourapplication import init_db >>> init_db() + +.. _sqlalchemy-pattern: + +SQLAlchemy in Flask +------------------- + +Many people prefer `SQLAlchemy`_ for database access. In this case it's +encouraged to use a package instead of a module for your flask application +and drop the modules into a separate module (:ref:`larger-applications`). +Although that is not necessary but makes a lot of sense. + +There are three very common ways to use SQLAlchemy. I will outline each +of them here: + +Declarative +``````````` + +The declarative extension in SQLAlchemy is the most recent method of using +SQLAlchemy. It allows you to define tables and models in one go, similar +to how Django works. In addition to the following text I recommend the +official documentation on the `declarative`_ extension. + +Here the example `database.py` module for your application:: + + from sqlalchemy import create_engine + from sqlalchemy.orm import scoped_session, sessionmaker + from sqlalchemy.ext.declarative import declarative_base + + engine = create_engine('sqlite:////tmp/test.db') + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + Base = declarative_base() + Base.query = db_session.query_property() + + def init_db(): + Base.metadata.create_all(bind=engine) + +To define your models, subclass the `Base` class the above code generated. + +To use SQLAlchemy in a declarative way with your application, you just +have to put the following code into your application module Flask will +automatically remove database sessions at the end of the request for you:: + + from yourapplication.database import db_session + + @app.after_request + def shutdown_session(response): + db_session.remove() + return response + +Here an example model (put that into `models.py` for instance):: + + from sqlalchemy import Column, Integer, String + from yourapplication.database import Base + + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True) + email = Column(String(120), unique=True) + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return '' % (self.name, self.email) + +You can insert entries into the database like this then: + +>>> from yourapplication.database import db_session +>>> from yourapplication.models import User +>>> u = User('admin', 'admin@localhost') +>>> db_session.add(u) +>>> db_session.commit() + +Querying is simple as well: + +>>> User.query.all() +[] +>>> User.query.filter(User.name == 'admin').first() + + +.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _declarative: + http://www.sqlalchemy.org/docs/reference/ext/declarative.html + + .. _template-inheritance: Template Inheritance From b608785801ce819037cb6cca04c70fd398519c08 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 12:23:46 +0200 Subject: [PATCH 0053/3747] Recentered README --- README | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README b/README index dc70692d..d4f57f2a 100644 --- a/README +++ b/README @@ -1,8 +1,8 @@ - // Flask // + // Flask // - because a pocket knife is not the only thing that - might come in handy + because a pocket knife is not the only thing that + might come in handy ~ What is Flask? From 61d07b8be9beae54d1c367adfc845dcf30a1512a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 12:29:52 +0200 Subject: [PATCH 0054/3747] Added note about decorators to becomingbig. --- docs/becomingbig.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 90cc0138..c964fe27 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -31,7 +31,10 @@ scale applications: - get rid of the decorator function registering which causes a lot of troubles for applications that have circular dependencies. It also requires that the whole application is imported when the system - initializes or certain URLs will not be available right away. + initializes or certain URLs will not be available right away. A + better solution would be to have one module with all URLs in there and + specifing the target functions explictliy or by name and importing + them when needed. - switch to explicit request object passing. This makes it more to type (because you now have something to pass around) but it makes it a whole lot easier to debug hairy situations and to test the code. From f54c4fd04f326d94684c7fcbf12df5cecae653c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 13:15:44 +0200 Subject: [PATCH 0055/3747] Fixed wording for circular imports --- docs/patterns.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns.rst b/docs/patterns.rst index 91e0d5b6..5956ab83 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -87,7 +87,7 @@ And this is what `views.py` would look like:: .. admonition:: Circular Imports Every Python programmer hates it, and yet we just did that: circular - imports (That's when one module depends on another one. In this case + imports (That's when two module depend on each one. In this case `views.py` depends on `__init__.py`). Be advised that this is a bad idea in general but here it is actually fine. The reason for this is that we are not actually using the views in `__init__.py` and just From 6418551efb1d24320c261b1e24c186d5394e6dcf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 13:29:01 +0200 Subject: [PATCH 0056/3747] More typo fixes --- docs/becomingbig.rst | 2 +- docs/patterns.rst | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index c964fe27..82d8c4a0 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -33,7 +33,7 @@ scale applications: also requires that the whole application is imported when the system initializes or certain URLs will not be available right away. A better solution would be to have one module with all URLs in there and - specifing the target functions explictliy or by name and importing + specifing the target functions explictly or by name and importing them when needed. - switch to explicit request object passing. This makes it more to type (because you now have something to pass around) but it makes it a diff --git a/docs/patterns.rst b/docs/patterns.rst index 5956ab83..7c2c85c6 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -195,7 +195,7 @@ SQLAlchemy in Flask Many people prefer `SQLAlchemy`_ for database access. In this case it's encouraged to use a package instead of a module for your flask application -and drop the modules into a separate module (:ref:`larger-applications`). +and drop the models into a separate module (:ref:`larger-applications`). Although that is not necessary but makes a lot of sense. There are three very common ways to use SQLAlchemy. I will outline each @@ -225,10 +225,11 @@ Here the example `database.py` module for your application:: def init_db(): Base.metadata.create_all(bind=engine) -To define your models, subclass the `Base` class the above code generated. +To define your models, just subclass the `Base` class that was created by +the code above. To use SQLAlchemy in a declarative way with your application, you just -have to put the following code into your application module Flask will +have to put the following code into your application module. Flask will automatically remove database sessions at the end of the request for you:: from yourapplication.database import db_session From 7c9303b7502403c372171e1e045433b57259a56b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 13:31:48 +0200 Subject: [PATCH 0057/3747] More typo fixes --- docs/becomingbig.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 82d8c4a0..c3c4a0b6 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -33,12 +33,12 @@ scale applications: also requires that the whole application is imported when the system initializes or certain URLs will not be available right away. A better solution would be to have one module with all URLs in there and - specifing the target functions explictly or by name and importing + specifing the target functions explicitly or by name and importing them when needed. - switch to explicit request object passing. This makes it more to type (because you now have something to pass around) but it makes it a whole lot easier to debug hairy situations and to test the code. -- integrate the `Babel`_ i18n package or `SQLAlchemy`_ directl into the +- integrate the `Babel`_ i18n package or `SQLAlchemy`_ directly into the core framework. .. _Babel: http://babel.edgewall.org/ From 6d556acb3fcaa6eeff4bda8cf704ad5f0cf5b5ac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 13:42:47 +0200 Subject: [PATCH 0058/3747] added coming soon to parts of the docs, linked scoped_session --- docs/conf.py | 3 ++- docs/patterns.rst | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9f417120..98db7c51 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -241,5 +241,6 @@ latex_documents = [ intersphinx_mapping = { 'http://docs.python.org/dev': None, - 'http://werkzeug.pocoo.org/documentation/dev/': None + 'http://werkzeug.pocoo.org/documentation/dev/': None, + 'http://www.sqlalchemy.org/docs/': None } diff --git a/docs/patterns.rst b/docs/patterns.rst index 7c2c85c6..e6bfc6c3 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -226,7 +226,10 @@ Here the example `database.py` module for your application:: Base.metadata.create_all(bind=engine) To define your models, just subclass the `Base` class that was created by -the code above. +the code above. If you are wondering why we don't have to care about +threads here (like we did in the SQLite3 example above with the +:data:`~flask.g` object): that's because SQLAlchemy does that for us +already with the :class:`~sqlalchemy.orm.scoped_session`. To use SQLAlchemy in a declarative way with your application, you just have to put the following code into your application module. Flask will @@ -276,6 +279,16 @@ Querying is simple as well: .. _declarative: http://www.sqlalchemy.org/docs/reference/ext/declarative.html +Manual Object Relational Mapping +-------------------------------- + +*coming soon* + +SQL Abstraction Layer +--------------------- + +*coming soon* + .. _template-inheritance: From 5348eff4457a528cd605bbbd5e0152c282819962 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 13:45:52 +0200 Subject: [PATCH 0059/3747] fixed headers --- docs/patterns.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns.rst b/docs/patterns.rst index e6bfc6c3..f226b78d 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -280,12 +280,12 @@ Querying is simple as well: http://www.sqlalchemy.org/docs/reference/ext/declarative.html Manual Object Relational Mapping --------------------------------- +```````````````````````````````` *coming soon* SQL Abstraction Layer ---------------------- +````````````````````` *coming soon* From 855e7e9327c0095f39b34bd56b08aa63c87f9dfd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:12:31 +0200 Subject: [PATCH 0060/3747] Added license file --- LICENSE | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6ed65ec8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From f769e15fecf6e06c190133d85a3de6b703e4d1a1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:17:24 +0200 Subject: [PATCH 0061/3747] Added website sources --- website/index.html | 56 +++++++++++++++++++++++++++++++++++++++++++++ website/logo.png | Bin 0 -> 23478 bytes 2 files changed, 56 insertions(+) create mode 100755 website/index.html create mode 100755 website/logo.png diff --git a/website/index.html b/website/index.html new file mode 100755 index 00000000..220b10ef --- /dev/null +++ b/website/index.html @@ -0,0 +1,56 @@ + +Flask (A Python Microframework) + + + +Fork me on GitHub diff --git a/website/logo.png b/website/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..f255eece8157a0ce6baf7e2e0b297c31ce153405 GIT binary patch literal 23478 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 S^>upy0000 Date: Fri, 16 Apr 2010 14:20:02 +0200 Subject: [PATCH 0062/3747] -x on files --- website/index.html | 0 website/logo.png | Bin 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 website/index.html mode change 100755 => 100644 website/logo.png diff --git a/website/index.html b/website/index.html old mode 100755 new mode 100644 diff --git a/website/logo.png b/website/logo.png old mode 100755 new mode 100644 From 0bb8b4f9a1c61fcf473e5ca7a10cc3ed4f30766f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:24:12 +0200 Subject: [PATCH 0063/3747] Beefed up setup.py --- setup.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/setup.py b/setup.py index e5f160ad..30b0e16f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,42 @@ +""" +Flask +----- + +Flask is a microframework for Python based on Werkzeug, Jinja 2 and good +intentions. And before you ask: It's BSD licensed! + +Flask is Fun +```````````` + +:: + + from flask import Flask + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello World!" + + if __name__ == "__main__": + app.run() + +And Easy to Setup +````````````````` + +:: + + $ easy_install Flask + $ python hello.py + * Running on http://localhost:5000/ + +Links +````` + +* `website `_ +* `documentation `_ +* `development version `_ + +""" from setuptools import setup @@ -9,11 +48,22 @@ setup( author='Armin Ronacher', author_email='armin.ronacher@active-4.com', description='A microframework based on Werkzeug, Jinja2 and good intentions', + long_description=__doc__, py_modules=['flask'], zip_safe=False, platforms='any', install_requires=[ 'Werkzeug>=0.6.1', 'Jinja2>=2.4' + ], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' ] ) From 8605cc310d260c3b08160881b09da26c2cc95f8d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:25:24 +0200 Subject: [PATCH 0064/3747] ignore dist folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0edef0e9..5250e072 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.pyc *.pyo env +dist *.egg-info From 9fbf9a062c11904b5022a5aa7941449e31340c32 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:32:59 +0200 Subject: [PATCH 0065/3747] head is 0.2-dev --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 30b0e16f..1742542d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ from setuptools import setup setup( name='Flask', - version='0.1', + version='0.2', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 791cdb28f593d4d7649d88407fcd193834512219 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Apr 2010 14:33:12 +0200 Subject: [PATCH 0066/3747] Fixed capitalization on website --- website/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/index.html b/website/index.html index 220b10ef..19c1f0b4 100644 --- a/website/index.html +++ b/website/index.html @@ -41,8 +41,8 @@ def hello():

    Interested?

    Contribute

    Found a bug? Have a good idea for improving Flask? Head over to From 5310fc38227b1907cb541f4a30ea8dfef46c2e19 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 14:10:47 +0200 Subject: [PATCH 0067/3747] Session falls back to a dummy object now if secret_key is missing. This makes it possible to still read-only access the empty session but requires the secret key to be set for write access. The error message raised explains that. This closes #10. --- flask.py | 23 ++++++++++++++++++++--- tests/flask_tests.py | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/flask.py b/flask.py index f7ba22e6..18e4a446 100644 --- a/flask.py +++ b/flask.py @@ -68,6 +68,22 @@ class _RequestGlobals(object): pass +class _NullSession(SecureCookie): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail + + + class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the @@ -80,6 +96,8 @@ class _RequestContext(object): self.url_adapter = app.url_map.bind_to_environ(environ) self.request = app.request_class(environ) self.session = app.open_session(self.request) + if self.session is None: + self.session = _NullSession() self.g = _RequestGlobals() self.flashes = None @@ -384,8 +402,7 @@ class Flask(object): object) :param response: an instance of :attr:`response_class` """ - if session is not None: - session.save_cookie(response, self.session_cookie_name) + session.save_cookie(response, self.session_cookie_name) def add_url_rule(self, rule, endpoint, **options): """Connects a URL rule. Works exactly like the :meth:`route` @@ -603,7 +620,7 @@ class Flask(object): instance of :attr:`response_class`. """ session = _request_ctx_stack.top.session - if session is not None: + if not isinstance(session, _NullSession): self.save_session(session, response) for handler in self.after_request_funcs: response = handler(response) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 0d73c954..b9edd366 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -72,6 +72,20 @@ class BasicFunctionality(unittest.TestCase): assert c.post('/set', data={'value': '42'}).data == 'value set' assert c.get('/get').data == '42' + def test_missing_session(self): + app = flask.Flask(__name__) + def expect_exception(f, *args, **kwargs): + try: + f(*args, **kwargs) + except RuntimeError, e: + assert e.args and 'session is unavailable' in e.args[0] + else: + assert False, 'expected exception' + with app.test_request_context(): + assert flask.session.get('missing_key') is None + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') + def test_request_processing(self): app = flask.Flask(__name__) evts = [] From b9cae3564ad31876d977c24a09d760e77ca992f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 14:24:12 +0200 Subject: [PATCH 0068/3747] Reordered deployment docs --- docs/deploying.rst | 253 +++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 126 deletions(-) diff --git a/docs/deploying.rst b/docs/deploying.rst index e04112f0..0d0ed19e 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -11,6 +11,133 @@ how to use a WSGI app with it. Just remember that your application object is the actual WSGI application. +mod_wsgi (Apache) +----------------- + +If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. + +.. _Apache: http://httpd.apache.org/ + +Installing `mod_wsgi` +````````````````````` + +If you don't have `mod_wsgi` installed yet you have to either install it using +a package manager or compile it yourself. + +The mod_wsgi `installation instructions`_ cover installation instructions for +source installations on UNIX systems. + +If you are using ubuntu / debian you can apt-get it and activate it as follows:: + + # apt-get install libapache2-mod-wsgi + +On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using +pkg_add:: + + # pkg_add -r mod_wsgi + +If you are using pkgsrc you can install `mod_wsgi` by compiling the +`www/ap2-wsgi` package. + +If you encounter segfaulting child processes after the first apache reload you +can safely ignore them. Just restart the server. + +Creating a `.wsgi` file +``````````````````````` + +To run your application you need a `yourapplication.wsgi` file. This file +contains the code `mod_wsgi` is executing on startup to get the application +object. The object called `application` in that file is then used as +application. + +For most applications the following file should be sufficient:: + + from yourapplication import app as application + +If you don't have a factory function for application creation but a singleton +instance you can directly import that one as `application`. + +Store that file somewhere where you will find it again (eg: +`/var/www/yourapplication`) and make sure that `yourapplication` and all +the libraries that are in use are on the python load path. If you don't +want to install it system wide consider using a `virtual python`_ instance. + +Configuring Apache +`````````````````` + +The last thing you have to do is to create an Apache configuration file for +your application. In this example we are telling `mod_wsgi` to execute the +application under a different user for security reasons: + +.. sourcecode:: apache + + + ServerName example.com + + WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 + WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi + + + WSGIProcessGroup yourapplication + WSGIApplicationGroup %{GLOBAL} + Order deny,allow + Allow from all + + + +For more information consult the `mod_wsgi wiki`_. + +.. _mod_wsgi: http://code.google.com/p/modwsgi/ +.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide +.. _virtual python: http://pypi.python.org/pypi/virtualenv +.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ + + +CGI +--- + +If all other deployment methods do not work, CGI will work for sure. CGI +is supported by all major servers but usually has a less-than-optimal +performance. + +This is also the way you can use a Flask application on Google's +`AppEngine`_, there however the execution does happen in a CGI-like +environment. The application's performance is unaffected because of that. + +.. _AppEngine: http://code.google.com/appengine/ + +Creating a `.cgi` file +`````````````````````` + +First you need to create the CGI application file. Let's call it +`yourapplication.cgi`:: + + #!/usr/bin/python + from wsgiref.handlers import CGIHandler + from yourapplication import app + + CGIHandler().run(app) + +If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python +2.5 and higher ship this as part of the standard library. + +Server Setup +```````````` + +Usually there are two ways to configure the server. Either just copy the +`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to +rewrite the URL) or let the server point to the file directly. + +In Apache for example you can put a like like this into the config: + +.. sourcecode:: apache + + ScriptName /app /path/to/the/application.cgi + +For more information consult the documentation of your webserver. + + + FastCGI ------- @@ -141,88 +268,6 @@ path. Common problems are: .. _flup: http://trac.saddi.com/flup -mod_wsgi (Apache) ------------------ - -If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. - -.. _Apache: http://httpd.apache.org/ - -Installing `mod_wsgi` -````````````````````` - -If you don't have `mod_wsgi` installed yet you have to either install it using -a package manager or compile it yourself. - -The mod_wsgi `installation instructions`_ cover installation instructions for -source installations on UNIX systems. - -If you are using ubuntu / debian you can apt-get it and activate it as follows:: - - # apt-get install libapache2-mod-wsgi - -On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using -pkg_add:: - - # pkg_add -r mod_wsgi - -If you are using pkgsrc you can install `mod_wsgi` by compiling the -`www/ap2-wsgi` package. - -If you encounter segfaulting child processes after the first apache reload you -can safely ignore them. Just restart the server. - -Creating a `.wsgi` file -``````````````````````` - -To run your application you need a `yourapplication.wsgi` file. This file -contains the code `mod_wsgi` is executing on startup to get the application -object. The object called `application` in that file is then used as -application. - -For most applications the following file should be sufficient:: - - from yourapplication import app as application - -If you don't have a factory function for application creation but a singleton -instance you can directly import that one as `application`. - -Store that file somewhere where you will find it again (eg: -`/var/www/yourapplication`) and make sure that `yourapplication` and all -the libraries that are in use are on the python load path. If you don't -want to install it system wide consider using a `virtual python`_ instance. - -Configuring Apache -`````````````````` - -The last thing you have to do is to create an Apache configuration file for -your application. In this example we are telling `mod_wsgi` to execute the -application under a different user for security reasons: - -.. sourcecode:: apache - - - ServerName example.com - - WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 - WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi - - - WSGIProcessGroup yourapplication - WSGIApplicationGroup %{GLOBAL} - Order deny,allow - Allow from all - - - -For more information consult the `mod_wsgi wiki`_. - -.. _mod_wsgi: http://code.google.com/p/modwsgi/ -.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide -.. _virtual python: http://pypi.python.org/pypi/virtualenv -.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ - - Tornado -------- @@ -260,47 +305,3 @@ Gevent .. _Gevent: http://www.gevent.org/ .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ - -CGI ---- - -If all other deployment methods do not work, CGI will work for sure. CGI -is supported by all major browsers but usually has a less-than-optimal -performance. - -This is also the way you can use a Flask application on Google's -`AppEngine`_, there however the execution does happen in a CGI-like -environment. The application's performance is unaffected because of that. - -.. _AppEngine: http://code.google.com/appengine/ - -Creating a `.cgi` file -`````````````````````` - -First you need to create the CGI application file. Let's call it -`yourapplication.cgi`:: - - #!/usr/bin/python - from wsgiref.handlers import CGIHandler - from yourapplication import app - - CGIHandler().run(app) - -If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python -2.5 and higher ship this as part of the standard library. - -Server Setup -```````````` - -Usually there are two ways to configure the server. Either just copy the -`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to -rewrite the URL) or let the server point to the file directly. - -In Apache for example you can put a like like this into the config: - -.. sourcecode:: apache - - ScriptName /app /path/to/the/application.cgi - -For more information consult the documentation of your webserver. - From 74862dcbca052ea0a994da3f507c00e9cc18821c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 14:30:28 +0200 Subject: [PATCH 0069/3747] Added makefile target for website and doc updating. --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index 0eabe5d8..7aaa77dc 100644 --- a/Makefile +++ b/Makefile @@ -9,3 +9,9 @@ clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + + +upload-website: + scp -r website/* pocoo.org:/var/www/flask.pocoo.org/ + +upload-docs: + $(MAKE) -C docs html && scp -r docs/_build/html/* pocoo.org:/var/www/flask.pocoo.org/docs/ From 0b9c2b5eb1b604cd76d9252e8e9628df731ea572 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 14:32:52 +0200 Subject: [PATCH 0070/3747] removed refactoring leftover --- flask.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask.py b/flask.py index 18e4a446..93d1dd9a 100644 --- a/flask.py +++ b/flask.py @@ -325,7 +325,6 @@ class Flask(object): :param context: the context as a dictionary that is updated in place to add extra variables. """ - reqctx = _request_ctx_stack.top for func in self.template_context_processors: context.update(func()) From fe6c279df12294e590e87705b0ba8728a3ed54c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 18:14:42 +0200 Subject: [PATCH 0071/3747] Synched tagline :) --- README | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README b/README index d4f57f2a..d648ef4e 100644 --- a/README +++ b/README @@ -1,8 +1,7 @@ // Flask // - because a pocket knife is not the only thing that - might come in handy + because sometimes a pocket knife is not enough ~ What is Flask? From 325dfa2469f86e03187b6d9e6aee4745b702c496 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Apr 2010 20:52:37 +0200 Subject: [PATCH 0072/3747] Larger fonts in the docs --- docs/_templates/sidebarintro.html | 1 + docs/_themes/flasky/static/flasky.css_t | 5 +-- website/index.html | 50 ++++++++++++++++++------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 06bd737e..b8381d7d 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -7,6 +7,7 @@

    Useful Links

    diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index a006b9b1..b7de089f 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -15,7 +15,7 @@ body { font-family: 'Georgia', serif; - font-size: 100%; + font-size: 16px; background-color: #555; color: #555; margin: 0; @@ -43,7 +43,6 @@ div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; - font-size: 0.9em; } div.footer { @@ -70,7 +69,7 @@ div.related a { } div.sphinxsidebar { - font-size: 0.75em; + font-size: 0.85em; line-height: 1.5em; } diff --git a/website/index.html b/website/index.html index 19c1f0b4..0c1bd059 100644 --- a/website/index.html +++ b/website/index.html @@ -1,21 +1,24 @@ Flask (A Python Microframework) +

    Flask

    @@ -37,18 +40,39 @@ def hello():

    And Easy to Setup

    $ easy_install Flask
     $ python hello.py
    - * Running on http://localhost:5000/
    + * Running on http://localhost:5000/

    Interested?

    +

    What’s in the Box?

    + +

    What do Flask Apps look like?

    +

    + If you are looking for some example code of applications written with Flask, + have a look at the sources of the examples on github: +

    Contribute

    Found a bug? Have a good idea for improving Flask? Head over to Flask's github page and - create a new ticket or fork. If you just want to chat with the - developers, go to #pocoo on irc.freenode.net + create a new ticket or fork. If you just want to chat with fellow + developers, go to #pocoo on irc.freenode.net. +

    From b4cd70d054a1514adaed893663cfa22b73114d1e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 03:07:08 +0200 Subject: [PATCH 0073/3747] Updated website --- website/404.html | 47 +++++++++++++++++++++++++++++++++++++++++++++++ website/mask.png | Bin 0 -> 1889 bytes website/ship.png | Bin 0 -> 62587 bytes 3 files changed, 47 insertions(+) create mode 100644 website/404.html create mode 100644 website/mask.png create mode 100644 website/ship.png diff --git a/website/404.html b/website/404.html new file mode 100644 index 00000000..7cc80b04 --- /dev/null +++ b/website/404.html @@ -0,0 +1,47 @@ + +Awwww. What you searched cannot be found + +

    Chapter 404: The Lost Page

    +

    A careful and diligent search has been made for the desired page, but it just cannot be found. +

    And so they returned to familiar waters. diff --git a/website/mask.png b/website/mask.png new file mode 100644 index 0000000000000000000000000000000000000000..f2fdbe14b0aab5d03a366ed491910ec6f3ab0688 GIT binary patch literal 1889 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

    F+w9=+U;Y2tlJk)x%NnDhpT;iDidr%j zgWW4Dz`raPaHqcv(AN@U=Ap|?L)*M>0K4$F!{`Qh z_&`S5CdNq$hbeloP&SAQjwcI6xds9b4g~>)8mry-#|^PLj}a2^?sCW=$jzKgVGY)! z$e~4_eD~GG;uj|A0pD-E0LqR)zwd@WI zcQNkZeMk`f7Q-^b@=L5m<}~J?gaelL3&AZ~{E(gwJHyW2#n( z3VRd41K9Hft1UU0+n0Y`4%EgO4CgUYf&e*fl3!#%I8OUh5C8x=0gfnc-}1&h3L3wTHVw Tn_3s>fRC=WzE<%iyZiqKKyJ}l literal 0 HcmV?d00001 From 42dd94b7565052120423dbd86a5ab0f96938f8cc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 03:08:16 +0200 Subject: [PATCH 0074/3747] Fixed title. --- website/404.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/404.html b/website/404.html index 7cc80b04..c4de489d 100644 --- a/website/404.html +++ b/website/404.html @@ -1,5 +1,5 @@ -Awwww. What you searched cannot be found +Chapter 404: The Lost Page - {% endblock %} - {% block content %} -

    Index

    -

    - Welcome on my awesome homepage. - {% endblock %} - -The ``{% extends %}`` tag is the key here. It tells the template engine that -this template "extends" another template. When the template system evaluates -this template, first it locates the parent. The extends tag must be the -first tag in the template. To render the contents of a block defined in -the parent template, use ``{{ super() }}``. - -.. _message-flashing-pattern: - -Message Flashing ----------------- - -Good applications and user interfaces are all about feedback. If the user -does not get enough feedback he will probably end up hating the -application. Flask provides a really simple way to give feedback to a -user with the flashing system. The flashing system basically makes it -possible to record a message at the end of a request and access it next -request and only next request. This is usually combined with a layout -template that does this. - -So here a full example:: - - from flask import flash, redirect, url_for, render_template - - @app.route('/') - def index(): - return render_template('index.html') - - @app.route('/login', methods=['GET', 'POST']) - def login(): - error = None - if request.method == 'POST': - if request.form['username'] != 'admin' or \ - request.form['password'] != 'secret': - error = 'Invalid credentials' - else: - flash('You were sucessfully logged in') - return redirect(url_for('index')) - return render_template('login.html', error=error) - -And here the ``layout.html`` template which does the magic: - -.. sourcecode:: html+jinja - - - My Application - {% with messages = get_flashed_messages() %} - {% if messages %} -

      - {% for message in messages %} -
    • {{ message }}
    • - {% endfor %} -
    - {% endif %} - {% endwith %} - {% block body %}{% endblock %} - -And here the index.html template: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

    Overview

    -

    Do you want to log in? - {% endblock %} - -And of course the login template: - -.. sourcecode:: html+jinja - - {% extends "layout.html" %} - {% block body %} -

    Login

    - {% if error %} -

    Error: {{ error }} - {% endif %} - -

    -
    Username: -
    -
    Password: -
    -
    -

    -

    - {% endblock %} diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst new file mode 100644 index 00000000..c54ef28f --- /dev/null +++ b/docs/patterns/flashing.rst @@ -0,0 +1,79 @@ +Message Flashing +================ + +Good applications and user interfaces are all about feedback. If the user +does not get enough feedback he will probably end up hating the +application. Flask provides a really simple way to give feedback to a +user with the flashing system. The flashing system basically makes it +possible to record a message at the end of a request and access it next +request and only next request. This is usually combined with a layout +template that does this. + +So here a full example:: + + from flask import flash, redirect, url_for, render_template + + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/login', methods=['GET', 'POST']) + def login(): + error = None + if request.method == 'POST': + if request.form['username'] != 'admin' or \ + request.form['password'] != 'secret': + error = 'Invalid credentials' + else: + flash('You were sucessfully logged in') + return redirect(url_for('index')) + return render_template('login.html', error=error) + +And here the ``layout.html`` template which does the magic: + +.. sourcecode:: html+jinja + + + My Application + {% with messages = get_flashed_messages() %} + {% if messages %} +
      + {% for message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} + {% endwith %} + {% block body %}{% endblock %} + +And here the index.html template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

    Overview

    +

    Do you want to log in? + {% endblock %} + +And of course the login template: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block body %} +

    Login

    + {% if error %} +

    Error: {{ error }} + {% endif %} +

    +
    +
    Username: +
    +
    Password: +
    +
    +

    +

    + {% endblock %} diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst new file mode 100644 index 00000000..beba9cb8 --- /dev/null +++ b/docs/patterns/index.rst @@ -0,0 +1,20 @@ +.. _patterns: + +Patterns for Flask +================== + +Certain things are common enough that the changes are high you will find +them in most web applications. For example quite a lot of applications +are using relational databases and user authentication. In that case, +changes are they will open a database connection at the beginning of the +request and get the information of the currently logged in user. At the +end of the request, the database connection is closed again. + +.. toctree:: + :maxdepth: 2 + + packages + sqlite3 + sqlalchemy + templateinheritance + flashing diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst new file mode 100644 index 00000000..d0b28932 --- /dev/null +++ b/docs/patterns/packages.rst @@ -0,0 +1,87 @@ +.. _larger-applications: + +Larger Applications +=================== + +For larger applications it's a good idea to use a package instead of a +module. That is quite simple. Imagine a small application looks like +this:: + + /yourapplication + /yourapplication.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + +To convert that into a larger one, just create a new folder +`yourapplication` inside the existing one and move everything below it. +Then rename `yourapplication.py` to `__init__.py`. (Make sure to delete +all `.pyc` files first, otherwise things would most likely break) + +You should then end up with something like that:: + + /yourapplication + /yourapplication + /__init__.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + +But how do you run your application now? The naive ``python +yourapplication/__init__.py`` will not work. Let's just say that Python +does not want modules in packages to be the startup file. But that is not +a big problem, just add a new file called `runserver.py` next to the inner +`yourapplication` folder with the following contents:: + + from yourapplication import app + app.run(debug=True) + +What did we gain from this? Now we can restructure the application a bit +into multiple modules. The only thing you have to remember is the +following quick checklist: + +1. the `Flask` application object creation have to be in the + `__init__.py` file. That way each module can import it safely and the + `__name__` variable will resole to the correct package. +2. all the view functions (the ones with a :meth:`~flask.Flask.route` + decorator on top) have to be imported when in the `__init__.py` file. + Not the objects itself, but the module it is in. Do the importing at + the *bottom* of the file. + +Here an example `__init__.py`:: + + from flask import Flask + app = Flask(__name__) + + import yourapplication.views + +And this is what `views.py` would look like:: + + from yourapplication import app + + @app.route('/') + def index(): + return 'Hello World!' + +.. admonition:: Circular Imports + + Every Python programmer hates them, and yet we just added some: + circular imports (That's when two module depend on each one. In this + case `views.py` depends on `__init__.py`). Be advised that this is a + bad idea in general but here it is actually fine. The reason for this + is + that we are not actually using the views in `__init__.py` and just + ensuring the module is imported and we are doing that at the bottom of + the file. + + There are still some problems with that approach but if you want to use + decorators there is no way around that. Check out the + :ref:`becomingbig` section for some inspiration how to deal with that. diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst new file mode 100644 index 00000000..65ad5a61 --- /dev/null +++ b/docs/patterns/sqlalchemy.rst @@ -0,0 +1,100 @@ +.. _sqlalchemy-pattern: + +SQLAlchemy in Flask +=================== + +Many people prefer `SQLAlchemy`_ for database access. In this case it's +encouraged to use a package instead of a module for your flask application +and drop the models into a separate module (:ref:`larger-applications`). +Although that is not necessary but makes a lot of sense. + +There are three very common ways to use SQLAlchemy. I will outline each +of them here: + +Declarative +----------- + +The declarative extension in SQLAlchemy is the most recent method of using +SQLAlchemy. It allows you to define tables and models in one go, similar +to how Django works. In addition to the following text I recommend the +official documentation on the `declarative`_ extension. + +Here the example `database.py` module for your application:: + + from sqlalchemy import create_engine + from sqlalchemy.orm import scoped_session, sessionmaker + from sqlalchemy.ext.declarative import declarative_base + + engine = create_engine('sqlite:////tmp/test.db') + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + Base = declarative_base() + Base.query = db_session.query_property() + + def init_db(): + Base.metadata.create_all(bind=engine) + +To define your models, just subclass the `Base` class that was created by +the code above. If you are wondering why we don't have to care about +threads here (like we did in the SQLite3 example above with the +:data:`~flask.g` object): that's because SQLAlchemy does that for us +already with the :class:`~sqlalchemy.orm.scoped_session`. + +To use SQLAlchemy in a declarative way with your application, you just +have to put the following code into your application module. Flask will +automatically remove database sessions at the end of the request for you:: + + from yourapplication.database import db_session + + @app.after_request + def shutdown_session(response): + db_session.remove() + return response + +Here an example model (put that into `models.py` for instance):: + + from sqlalchemy import Column, Integer, String + from yourapplication.database import Base + + class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(50), unique=True) + email = Column(String(120), unique=True) + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return '' % (self.name, self.email) + +You can insert entries into the database like this then: + +>>> from yourapplication.database import db_session +>>> from yourapplication.models import User +>>> u = User('admin', 'admin@localhost') +>>> db_session.add(u) +>>> db_session.commit() + +Querying is simple as well: + +>>> User.query.all() +[] +>>> User.query.filter(User.name == 'admin').first() + + +.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _declarative: + http://www.sqlalchemy.org/docs/reference/ext/declarative.html + +Manual Object Relational Mapping +-------------------------------- + +*coming soon* + +SQL Abstraction Layer +--------------------- + +*coming soon* diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst new file mode 100644 index 00000000..3e758002 --- /dev/null +++ b/docs/patterns/sqlite3.rst @@ -0,0 +1,87 @@ +.. _sqlite3: + +Using SQLite 3 with Flask +========================= + +In Flask you can implement opening of dabase connections at the beginning +of the request and closing at the end with the +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` +decorators in combination with the special :class:`~flask.g` object. + +So here a simple example how you can use SQLite 3 with Flask:: + + import sqlite3 + from flask import g + + DATABASE = '/path/to/database.db' + + def connect_db(): + return sqlite3.connect(DATABASE) + + @app.before_request + def before_request(): + g.db = connect_db() + + @app.after_request + def after_request(response): + g.db.close() + return response + +.. _easy-querying: + +Easy Querying +------------- + +Now in each request handling function you can access `g.db` to get the +current open database connection. To simplify working with SQLite a +helper function can be useful:: + + def query_db(query, args=(), one=False): + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] + return (rv[0] if rv else None) if one else rv + +This handy little function makes working with the database much more +pleasant than it is by just using the raw cursor and connection objects. + +Here is how you can use it:: + + for user in query_db('select * from users'): + print user['username'], 'has the id', user['user_id'] + +Or if you just want a single result:: + + user = query_db('select * from users where username = ?', + [the_username], one=True) + if user is None: + print 'No such user' + else: + print the_username, 'has the id', user['user_id'] + +To pass variable parts to the SQL statement, use a question mark in the +statement and pass in the arguments as a list. Never directly add them to +the SQL statement with string formattings because this makes it possible +to attack the application using `SQL Injections +`_. + +Initial Schemas +--------------- + +Relational databases need schemas, so applications often ship a +`schema.sql` file that creates the database. It's a good idea to provide +a function that creates the database bases on that schema. This function +can do that for you:: + + from contextlib import closing + + def init_db(): + with closing(connect_db()) as db: + with app.open_resource('schema.sql') as f: + db.cursor().executescript(f.read()) + db.commit() + +You can then create such a database from the python shell: + +>>> from yourapplication import init_db +>>> init_db() diff --git a/docs/patterns/templateinheritance.rst b/docs/patterns/templateinheritance.rst new file mode 100644 index 00000000..8a1a306d --- /dev/null +++ b/docs/patterns/templateinheritance.rst @@ -0,0 +1,69 @@ +.. _template-inheritance: + +Template Inheritance +==================== + +The most powerful part of Jinja is template inheritance. Template inheritance +allows you to build a base "skeleton" template that contains all the common +elements of your site and defines **blocks** that child templates can override. + +Sounds complicated but is very basic. It's easiest to understand it by starting +with an example. + + +Base Template +------------- + +This template, which we'll call ``layout.html``, defines a simple HTML skeleton +document that you might use for a simple two-column page. It's the job of +"child" templates to fill the empty blocks with content: + +.. sourcecode:: html+jinja + + + + + {% block head %} + + {% block title %}{% endblock %} - My Webpage + {% endblock %} + + +
    {% block content %}{% endblock %}
    + + + +In this example, the ``{% block %}`` tags define four blocks that child templates +can fill in. All the `block` tag does is to tell the template engine that a +child template may override those portions of the template. + +Child Template +-------------- + +A child template might look like this: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block title %}Index{% endblock %} + {% block head %} + {{ super() }} + + {% endblock %} + {% block content %} +

    Index

    +

    + Welcome on my awesome homepage. + {% endblock %} + +The ``{% extends %}`` tag is the key here. It tells the template engine that +this template "extends" another template. When the template system evaluates +this template, first it locates the parent. The extends tag must be the +first tag in the template. To render the contents of a block defined in +the parent template, use ``{{ super() }}``. From 42a46ed09b745c0560de354c0cb236295000f296 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 13:58:48 +0200 Subject: [PATCH 0080/3747] Fixed homepage links --- website/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/index.html b/website/index.html index 7dbd33ba..ae835ac2 100644 --- a/website/index.html +++ b/website/index.html @@ -50,11 +50,11 @@ def hello():

    What’s in the Box?

    From 63593d281b7358a86a808cc0bf5e47107316403d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 14:39:27 +0200 Subject: [PATCH 0081/3747] removed unused import --- flask.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask.py b/flask.py index 93d1dd9a..0564aca3 100644 --- a/flask.py +++ b/flask.py @@ -13,7 +13,6 @@ from __future__ import with_statement import os import sys -from threading import local from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, cached_property, \ From ab5492418694b5cc14601a953084d61f47785ea4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 14:56:08 +0200 Subject: [PATCH 0082/3747] Fixed a font name. --- docs/_themes/flasky/static/flasky.css_t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index 002a7564..04c5c5f6 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -160,7 +160,7 @@ div.body h3, div.body h4, div.body h5, div.body h6 { - font-family: 'Garamond', 'Georiga', serif; + font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; @@ -197,7 +197,7 @@ div.admonition { } div.admonition p.admonition-title { - font-family: 'Garamond', 'Georiga', serif; + font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; From 190059c8f01c6f75243824e765d08a7cce386eda Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 15:44:06 +0200 Subject: [PATCH 0083/3747] Added support for macro pulling and documented certain design decisions. --- docs/api.rst | 2 + docs/design.rst | 147 ++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + flask.py | 31 +++++++- tests/flask_tests.py | 6 ++ tests/templates/_macro.html | 1 + 6 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 docs/design.rst create mode 100644 tests/templates/_macro.html diff --git a/docs/api.rst b/docs/api.rst index e3439393..5dcdfa95 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -207,3 +207,5 @@ Template Rendering .. autofunction:: render_template .. autofunction:: render_template_string + +.. autofunction:: get_template_attribute diff --git a/docs/design.rst b/docs/design.rst new file mode 100644 index 00000000..ae1fd8d0 --- /dev/null +++ b/docs/design.rst @@ -0,0 +1,147 @@ +Design Decisions in Flask +========================= + +If you are curious why Flask does certain things the way it does and not +different, this section is for you. This should give you an idea about +some of the design decisions that may appear arbitrary and surprising at +first, especially in direct comparison with other frameworks. + + +The Explicit Application Object +------------------------------- + +A Python web application based on WSGI has to have one central callable +object that implements the actual application. In Flask this is an +instance of the :class:`~flask.Flask` class. Each Flask application has +to create an instance of this class itself and pass it the name of the +module, but why can't Flask do that itself? + +Without such an explicit application object the following code:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + return 'Hello World!' + +Would look like this instead:: + + from hypothetical_flask import route + + @route('/') + def index(): + return 'Hello World!' + +There are three major reasons for this. The most important one is that +implicit application objects require that there may only be one class at +the time. There are ways to fake multiple application with a single +application object, like maintaining a stack of applications, but this +causes some problems I won't outline here in detail. Now the question is: +when does a microframework need more than one application at the same +time? A good example for this is unittesting. When you want to test +something it can be very helpful to create a minimal application to test +specific behavior. When the application object is deleted everything it +allocated will be freed again. + +Another thing that becomes possible with having an explicit object laying +around in your code is that you can subclass the base class +(:class:`~flask.Flask`) to alter specific behaviour. This would not be +possible without hacks if the object was created ahead of time for you +based on a class that is not exposed to you. + +But there is another very important reason why Flask depends on an +explicit instanciation of that class: the package name. Whenever you +create a Flask instance you usually pass it `__name__` as package name. +Flask depends on that information to properly load resources relative +to your module. With Python's outstanding support for reflection it can +then access the package to figure out where the templates and static files +are stored (see :meth:`~flask.Flask.open_resource`). Now obviously there +are frameworks around that do not need any configuration and will still be +able to load templates relative to your application module. But they have +to use the current working directory for that, which is a very unreliable +way to determine where the application is. The current working directory +is process-wide and if you are running multiple applications in one +process (which could happen in a webserver without you knowing) the paths +will be off. Worse: many webservers do not set the working directory to +the directory of your application but to the document root which does not +have to be the same folder. + +The third reason is "explicit is better than implicit". That object is +your WSGI application, you don't have to remember anything else. If you +want to apply a WSGI middleware, just wrap it and you're done (though +there are better ways to do that so that you do not lose the reference +to the application object :meth:`~flask.Flask.wsgi_app`). + +One Template Engine +------------------- + +Flask decides on one template engine: Jinja2. Why doesn't Flask have a +pluggable template engine interface? You can obviously use a different +template engine, but Flask will still configure Jinja2 for you. While +that limitation that Jinja2 is *always* configured will probably go away, +the decision to bundle one template engine and use that will not. + +Template engines are like programming languages and each of those engines +has a certain understandment about how things work. On the surface they +all work the same: you tell the engine to evaluate a template with a set +of variables and take the return value as string. + +But that's about where similarities end. Jinja2 for example has an +extensive filter system, a certain way to do template inheritance, support +for reusable blocks (macros) that can be used from inside templates and +also from Python code, is using unicode for all operations, supports +iterative template rendering, configurable syntax and more. On the other +hand an engine like Genshi is based on XML stream evaluation, template +inheritance by taking the availability of XPath into account and more. +Mako on the other hand treats templates similar to Python modules. + +When it comes to bridge a template engine with an application or framework +there is more than just rendering templates. Flask uses Jinja2's +extensive autoescaping support for instance. Also it provides ways to +access macros from Jinja2 templates. + +A template abstraction layer that would not take the unique features of +the template engines away is a science on its own and a too large +undertaking for a microframework like Flask. + + +Micro with Dependencies +----------------------- + +Why does Flask call itself a microframework and yet it depends on two +libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look +over to the Ruby side of web development there we have a protocol very +similar to WSGI. Just that it's called Rack there, but besides that it +looks very much like a WSGI rendition for Ruby. But nearly all +applications in Ruby land do not work with Rack directly, but on top of a +lirbary with the same name. This Rack library has two equivalents in +Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but +from my understanding it's sortof deprecated in favour of WebOb. The +development of WebOb and Werkzeug started side by side with similar ideas +in mind: be a good implementation of WSGI for other applications to take +advantage. + +Flask is a framework that takes advantage of the work already done by +Werkzeug to properly interface WSGI (which can be a complex task at +times). Thanks to recent developments in the Python package +infrastructure, packages with depencencies are no longer an issue and +there are very few reasons against having libraries that depend on others. + + +Thread Locals +------------- + +Flask uses thread local objects (context local objects in fact, they +support greenlet contexts as well) for request, session and an extra +object you can put your own things on (:data:`~flask.g`). Why is that and +isn't that a bad idea? + +Yes it is usually not such a bright idea to use thread locals. They cause +troubles for servers that are not based on the concept of threads and make +large applications harder to maintain. However Flask is just not designed +for large applications or asyncronous servers. Flask wants to make it +quick and easy to write a traditional web application. + +Also see the :ref:`becomingbig` section of the documentation for some +inspiration for larger applications based on Flask. diff --git a/docs/index.rst b/docs/index.rst index 7f53ef67..06d8a4e8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ web development. patterns/index deploying becomingbig + design Reference --------- diff --git a/flask.py b/flask.py index 0564aca3..56ce49cc 100644 --- a/flask.py +++ b/flask.py @@ -120,6 +120,27 @@ def url_for(endpoint, **values): return _request_ctx_stack.top.url_adapter.build(endpoint, values) +def get_template_attribute(template_name, attribute): + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named `_foo.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_foo.html', 'hello') + return hello('World') + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to acccess + """ + return getattr(current_app.jinja_env.get_template(template_name).module, + attribute) + + def flash(message): """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, @@ -626,10 +647,18 @@ class Flask(object): def wsgi_app(self, environ, start_response): """The actual WSGI application. This is not implemented in - `__call__` so that middlewares can be applied: + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: app.wsgi_app = MyMiddleware(app.wsgi_app) + Then you still have the original application object around and + can continue to call methods on it. + :param environ: a WSGI environment :param start_response: a callable accepting a status code, a list of headers and an optional diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b9edd366..bb560712 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -197,6 +197,12 @@ class Templating(unittest.TestCase): '

    Hello World!' ] + def test_macros(self): + app = flask.Flask(__name__) + with app.test_request_context(): + macro = flask.get_template_attribute('_macro.html', 'hello') + assert macro('World') == 'Hello World!' + if __name__ == '__main__': unittest.main() diff --git a/tests/templates/_macro.html b/tests/templates/_macro.html new file mode 100644 index 00000000..3460ae2e --- /dev/null +++ b/tests/templates/_macro.html @@ -0,0 +1 @@ +{% macro hello(name) %}Hello {{ name }}!{% endmacro %} From bf52a17902cdbc300ee91ec1b2e8d2b7890ba35c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 16:29:41 +0200 Subject: [PATCH 0084/3747] Removed useless code --- docs/testing.rst | 1 - examples/flaskr/flaskr_tests.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 62b309ce..5439fbaf 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -162,7 +162,6 @@ like this:: text='HTML allowed here' ), follow_redirects=True) assert 'No entries here so far' not in rv.data - self.login(flaskr.USERNAME, flaskr.PASSWORD) assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index e8f01437..4355a650 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -57,7 +57,6 @@ class FlaskrTestCase(unittest.TestCase): text='HTML allowed here' ), follow_redirects=True) assert 'No entries here so far' not in rv.data - self.login(flaskr.USERNAME, flaskr.PASSWORD) assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data From 13090389365184fc2291e2cc69fb9b2af5426585 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 17:32:23 +0200 Subject: [PATCH 0085/3747] updated README --- README | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README b/README index d648ef4e..537a71c5 100644 --- a/README +++ b/README @@ -1,7 +1,7 @@ // Flask // - because sometimes a pocket knife is not enough + because sometimes a pocket knife is not enough ~ What is Flask? @@ -12,8 +12,10 @@ ~ Is it ready? - Nope, this is still work in progress, but I am happy to - accept patches and improvements already. + A preview release is out now, and I'm hoping for some + input about what you want from a microframework and + how it should look like. Consider the API to slightly + improve over time. ~ What do I need? From 94cf620914615b991063f00314ea1c8da9790c99 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 18:29:55 +0200 Subject: [PATCH 0086/3747] Updated SQLAlchemy docs. --- docs/patterns/sqlalchemy.rst | 94 +++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 65ad5a61..8d06187f 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -92,9 +92,99 @@ Querying is simple as well: Manual Object Relational Mapping -------------------------------- -*coming soon* +Manual object relational mapping has a few upsides and a few downsides +versus the declarative approach from above. The main difference is that +you define tables and classes separately and map them together. It's more +flexible but a little more to type. In general it works similar to the +declarative approach, so make sure to also split up your application into +multiple modules in a package. + +Here the example `database.py` module for your application:: + + from sqlalchemy import create_engine, MetaData + from sqlalchemy.orm import scoped_session, sessionmaker + + engine = create_engine('sqlite:////tmp/test.db') + metadata = MetaData() + db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) + def init_db(): + metadata.create_all(bind=engine) + +As for the declarative approach you need to close down the session after +each request. Put this into your application module:: + + from yourapplication.database import db_session + + @app.after_request + def shutdown_session(response): + db_session.remove() + return response + +Here an example table and model (put that into `models.py` for instance):: + + from sqlalchemy import Table, Column, Integer, String + from sqlalchemy.orm import mapper + from yourapplication.database import metadata, db_session + + class User(object): + query = db_session.query_property() + + def __init__(self, name=None, email=None): + self.name = name + self.email = email + + def __repr__(self): + return '' % (self.name, self.email) + + users = Table('users', metadata, + Column('id', Integer, primary_key=True), + Column('name', String(50), unique=True), + Column('email', String(120), unique=True) + ) + mapper(User, users) + +Querying and inserting works exactly the same as in the example above. + SQL Abstraction Layer --------------------- -*coming soon* +If you just want to use the database system (and SQL) abstraction layer +you basically only need the engine:: + + from sqlalchemy import create_engine, MetaData + + engine = create_engine('sqlite:////tmp/test.db') + metadata = MetaData(bind=engine) + +Then you can either declare the tables in your code like in the examples +above, or automatically load them:: + + users = Table('users', metadata, autoload=True) + +To insert data you can use the `insert` method. We have to get a +connection first so that we can use a transaction: + +>>> con = engine.connect() +>>> con.execute(users.insert(name='admin', email='admin@localhost')) + +SQLAlchemy will automatically commit for us. + +To query your database, yu use the engine directly or use a connection: + +>>> users.select(users.c.id == 1).execute().first() +(1, u'admin', u'admin@localhost') + +These results are also dict-like tuples: + +>>> r = users.select(users.c.id == 1).execute().first() +>>> r['name'] +u'admin' + +You can also pass string of SQL statements to the +:meth:`~sqlalchemy.engine.base.Connection.execute` method: + +>>> engine.execute('select * from users where id = :1', [1]).first() +(1, u'admin', u'admin@localhost') From 100776147a063dcf53f74678b045ac0f5d6386d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 18:40:52 +0200 Subject: [PATCH 0087/3747] Added troubleshooting section to the tutorial --- docs/tutorial/dbinit.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 80341d63..0dc87d58 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -55,3 +55,9 @@ importing and calling that function:: >>> from flaskr import init_db >>> init_db() + +.. admonition:: Troubleshooting + + If you get an exception later that a table cannot be found check that + you did call the `init_db` function and that your table names are + correct (singular vs. plural for example). From 3491465d55f093a498f2f496cd33bebba137518c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 20:43:18 +0200 Subject: [PATCH 0088/3747] No sudo for windows users. --- docs/installation.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index f83d9c28..a56ae3af 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -53,7 +53,8 @@ python-virtualenv``). If you are on Windows and missing the `easy_install` command you have to install it first. Check the :ref:`windows-easy-install` section for more -information about how to do that. +information about how to do that. Once you have it installed, run the +same commands as above, but without the `sudo` part. So now that you have virtualenv running just fire up a shell and create your own environment. I usually create a folder and a `env` folder From 0770f5c6375e3e45fa3ac918217df5d885176173 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 23:37:03 +0200 Subject: [PATCH 0089/3747] Removed two unused imports. --- flask.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flask.py b/flask.py index 56ce49cc..cec1aafb 100644 --- a/flask.py +++ b/flask.py @@ -15,10 +15,9 @@ import sys from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, cached_property, \ - SharedDataMiddleware + LocalStack, LocalProxy, create_environ, SharedDataMiddleware from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException from werkzeug.contrib.securecookie import SecureCookie # utilities we import from Werkzeug and Jinja2 that are unused From 73a47a0db1a181bb305684115d3dacded0c5a58b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 23:42:11 +0200 Subject: [PATCH 0090/3747] Removed unused stuff from minitwit and fixed a bug. --- examples/minitwit/minitwit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index c053e27f..ef0c7673 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for more details. """ from __future__ import with_statement -import re import time import sqlite3 from hashlib import md5 @@ -96,7 +95,6 @@ def timeline(): """ if not g.user: return redirect(url_for('public_timeline')) - offset = request.args.get('offset', type=int) return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where message.author_id = user.user_id and ( @@ -123,7 +121,7 @@ def user_timeline(username): [username], one=True) if profile_user is None: abort(404) - followd = False + followed = False if g.user: followed = query_db('''select 1 from follower where follower.who_id = ? and follower.whom_id = ?''', From ce18d4575dd8468878668d777d83448eeae4fd0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 02:22:59 +0200 Subject: [PATCH 0091/3747] Added WTForms documentation. --- docs/conf.py | 3 +- docs/installation.rst | 8 --- docs/patterns/flashing.rst | 2 + docs/patterns/index.rst | 1 + docs/patterns/wtforms.rst | 109 +++++++++++++++++++++++++++++++++++++ docs/quickstart.rst | 2 +- 6 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 docs/patterns/wtforms.rst diff --git a/docs/conf.py b/docs/conf.py index 98db7c51..03e27217 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -242,5 +242,6 @@ latex_documents = [ intersphinx_mapping = { 'http://docs.python.org/dev': None, 'http://werkzeug.pocoo.org/documentation/dev/': None, - 'http://www.sqlalchemy.org/docs/': None + 'http://www.sqlalchemy.org/docs/': None, + 'http://wtforms.simplecodes.com/docs/0.5/': None } diff --git a/docs/installation.rst b/docs/installation.rst index a56ae3af..c5788be2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -96,14 +96,6 @@ This is possible as well, but I would not recommend it. Just run (Run it in an Admin shell on Windows systems and without the `sudo`). - -The Drop into Place Version ---------------------------- - -Now I really don't recommend this way on using Flask, but you can do that -of course as well. Download the `dip` zipfile from the website and unzip -it next to your application. - .. _windows-easy-install: `easy_install` on Windows diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index c54ef28f..fca9a9e1 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -1,3 +1,5 @@ +.. _message-flashing-pattern: + Message Flashing ================ diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index beba9cb8..037f6e11 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -16,5 +16,6 @@ end of the request, the database connection is closed again. packages sqlite3 sqlalchemy + wtforms templateinheritance flashing diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst new file mode 100644 index 00000000..e79f9d3a --- /dev/null +++ b/docs/patterns/wtforms.rst @@ -0,0 +1,109 @@ +Form Validation with WTForms +============================ + +When you have to work with form data submitted by a browser view code +quickly becomes very hard to read. There are libraries out there designed +to make this process easier to manage. One of them is WTForms which we +will handle here. If you find yourself in the situation of having many +forms, you might want to give it a try. + +When you are working with WTForms you have to define your forms as classes +first. I recommend breaking up the application into multiple modules +(:ref:`larger-applications`) for that and adding a separate module for the +forms. + +The Forms +--------- + +This is an example form for a typical registration page:: + + from wtforms import Form, BooleanField, TextField, validators + + class RegistrationForm(Form): + username = TextField('Username', [validators.Length(min=4, max=25)]) + email = TextField('Email Address', [validators.Length(min=6, max=35)]) + password = PasswordField('New Password', [Required(), + EqualTo('confirm', mesage='Passwords must match')]) + confirm = PasswordField('Repeat Password') + accept_tos = BooleanField('I accept the TOS', [validators.Required()]) + +In the View +----------- + +In the view function, the usage of this form looks like this:: + + @app.route('/register', methods=['GET', 'POST']) + def register(): + form = RegistrationForm(request.form) + if request.method == 'POST' and form.validate(): + user = User(form.username.data, form.email.data, + form.password.data) + db_session.add(user) + flash('Thanks for registering') + redirect(url_for('login')) + return render_template('register.html', form=form) + +Notice that we are implying that the view is using SQLAlchemy here +(:ref:`sqlalchemy-pattern`) but this is no requirement of course. Adapt +the code as necessary. + +Things to remember: + +1. create the form from the request :attr:`~flask.request.form` value if + the data is submitted via the HTTP `POST` method and + :attr:`~flask.request.args` if the data is submitted as `GET`. +2. to validate the data, call the :func:`~wtforms.form.Form.validate` + method which will return `True` if the data validates, `False` + otherwise. +3. to access individual values from the form, access `form..data`. + +Forms in Templates +------------------ + +Now to the template side. When you pass the form to the templates you can +easily render them there. Look at the following example template to see +how easy this is. WTForms does half the form generation for us already. +To make it even nicer, we can write a macro that renders a field with +label and a list of errors if there are any. + +Here an example `_formhelpers.html` template with such a macro: + +.. sourcecode:: html+jinja + + {% macro render_field(field) %} +

    +

    Flask

    +

    because sometimes a pocket knife is not enough +

    + Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions. + And before you ask: It's BSD licensed! +
    +

    Flask is Fun

    +
    from flask import Flask
    +app = Flask(__name__)
    +
    +@app.route("/")
    +def hello():
    +    return "Hello World!"
    +
    +if __name__ == "__main__":
    +    app.run()
    +

    And Easy to Setup

    +
    $ easy_install Flask
    +$ python hello.py
    + * Running on http://localhost:5000/
    +

    Interested?

    + +

    Contribute

    +

    Found a bug? Have a good idea for improving Flask? Head over to + Flask's github page and + create a new ticket or fork. If you just want to chat with the + developers, go to #pocoo on irc.freenode.net +

    {{ field.label }} +
    {{ field(**kwargs)|safe }} + {% if field.errors %} +
      + {% for error in field.errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} +
    + {% endmacro %} + +This macro accepts a couple of keyword arguments that are forwarded to +WTForm's field function that renders the field for us. They keyword +arguments will be inserted as HTML attributes. So for example you can +call ``render_field(form.username, class='username')`` to add a class to +the input element. Note that WTForms returns standard Python unicode +strings, so we have to tell Jinja2 that this data is already HTML escaped +with the `|safe` filter. + +Here the `register.html` template for the function we used above which +takes advantage of the `_formhelpers.html` template: + +.. sourcecode:: html+jinja + + {% from "_formhelpers.html" import render_field %} +
    +
    + {{ render_field(form.username) }} + {{ render_field(form.email) }} + {{ render_field(form.password) }} + {{ render_field(form.confirm) }} + {{ render_field(form.accept_tos) }} +
    +

    +

    diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 944b3955..3d3e765f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -368,7 +368,7 @@ u'Marked up \xbb HTML' .. [#] Unsure what that :class:`~flask.g` object is? It's something you can store information on yourself, check the documentation of that - object (:class:`~flask.g`) and the :ref:`database-pattern` for more + object (:class:`~flask.g`) and the :ref:`sqlite3` for more information. From 976e792336fa6c4696f534ac1d537d2a86a75e72 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 02:24:02 +0200 Subject: [PATCH 0092/3747] Added links for SQLAlchemy and WTForms. --- docs/patterns/sqlalchemy.rst | 3 +++ docs/patterns/wtforms.rst | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 8d06187f..5a064469 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -188,3 +188,6 @@ You can also pass string of SQL statements to the >>> engine.execute('select * from users where id = :1', [1]).first() (1, u'admin', u'admin@localhost') + +For more information about SQLAlchemy, head over to the +`website `_. diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index e79f9d3a..0d6a313e 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -107,3 +107,8 @@ takes advantage of the `_formhelpers.html` template:

    + +For more information about WTForms, head over to the `WTForms +website`_. + +.. _WTForms website: http://wtforms.simplecodes.com/ From 56675b3bade603a458d01443bda8070ca0489005 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 02:25:31 +0200 Subject: [PATCH 0093/3747] Fixed indentation of an example. --- docs/patterns/wtforms.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 0d6a313e..262d364f 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -73,14 +73,12 @@ Here an example `_formhelpers.html` template with such a macro: {% macro render_field(field) %}

    {{ field.label }}
    {{ field(**kwargs)|safe }} - {% if field.errors %} -
      - {% for error in field.errors %} -
    • {{ error }}
    • - {% endfor %} -
    - {% endif %} -
    + {% if field.errors %} +
      + {% for error in field.errors %}
    • {{ error }}{% endfor %} +
    + {% endif %} + {% endmacro %} This macro accepts a couple of keyword arguments that are forwarded to From 166f5ed254d829b2d87fca241e1a22b0a64fff1c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 02:28:05 +0200 Subject: [PATCH 0094/3747] Forgot to link the website. --- docs/patterns/wtforms.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 262d364f..2a0b4ac8 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -3,7 +3,7 @@ Form Validation with WTForms When you have to work with form data submitted by a browser view code quickly becomes very hard to read. There are libraries out there designed -to make this process easier to manage. One of them is WTForms which we +to make this process easier to manage. One of them is `WTForms`_ which we will handle here. If you find yourself in the situation of having many forms, you might want to give it a try. @@ -109,4 +109,5 @@ takes advantage of the `_formhelpers.html` template: For more information about WTForms, head over to the `WTForms website`_. +.. _WTForms: http://wtforms.simplecodes.com/ .. _WTForms website: http://wtforms.simplecodes.com/ From 84899948be190a5e180fdccb25e6d7a8498fee7c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 02:45:44 +0200 Subject: [PATCH 0095/3747] Corrected order of the tutorial --- docs/tutorial/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index da37cf7a..3f2d659e 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -24,8 +24,8 @@ the `example source`_. folders schema setup - dbcon dbinit + dbcon views templates css From a50a87c4ca7f44821e9561ae6b69300b8809c46c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 10:37:10 +0200 Subject: [PATCH 0096/3747] ScriptName -> ScriptAlias --- docs/deploying.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying.rst b/docs/deploying.rst index 0d0ed19e..30e43888 100644 --- a/docs/deploying.rst +++ b/docs/deploying.rst @@ -132,7 +132,7 @@ In Apache for example you can put a like like this into the config: .. sourcecode:: apache - ScriptName /app /path/to/the/application.cgi + ScriptAlias /app /path/to/the/application.cgi For more information consult the documentation of your webserver. From c64a4e0befc0718500c544f5c847d6632b8fb598 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 11:23:44 +0200 Subject: [PATCH 0097/3747] Break up deployment docs into separate documents. --- docs/deploying.rst | 307 ------------------------------------ docs/deploying/cgi.rst | 42 +++++ docs/deploying/fastcgi.rst | 128 +++++++++++++++ docs/deploying/index.rst | 19 +++ docs/deploying/mod_wsgi.rst | 80 ++++++++++ docs/deploying/others.rst | 48 ++++++ docs/index.rst | 2 +- 7 files changed, 318 insertions(+), 308 deletions(-) delete mode 100644 docs/deploying.rst create mode 100644 docs/deploying/cgi.rst create mode 100644 docs/deploying/fastcgi.rst create mode 100644 docs/deploying/index.rst create mode 100644 docs/deploying/mod_wsgi.rst create mode 100644 docs/deploying/others.rst diff --git a/docs/deploying.rst b/docs/deploying.rst deleted file mode 100644 index 30e43888..00000000 --- a/docs/deploying.rst +++ /dev/null @@ -1,307 +0,0 @@ -Deployment Options -================== - -Depending on what you have available there are multiple ways to run Flask -applications. A very common method is to use the builtin server during -development and maybe behind a proxy for simple applications, but there -are more options available. - -If you have a different WSGI server look up the server documentation about -how to use a WSGI app with it. Just remember that your application object -is the actual WSGI application. - - -mod_wsgi (Apache) ------------------ - -If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. - -.. _Apache: http://httpd.apache.org/ - -Installing `mod_wsgi` -````````````````````` - -If you don't have `mod_wsgi` installed yet you have to either install it using -a package manager or compile it yourself. - -The mod_wsgi `installation instructions`_ cover installation instructions for -source installations on UNIX systems. - -If you are using ubuntu / debian you can apt-get it and activate it as follows:: - - # apt-get install libapache2-mod-wsgi - -On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using -pkg_add:: - - # pkg_add -r mod_wsgi - -If you are using pkgsrc you can install `mod_wsgi` by compiling the -`www/ap2-wsgi` package. - -If you encounter segfaulting child processes after the first apache reload you -can safely ignore them. Just restart the server. - -Creating a `.wsgi` file -``````````````````````` - -To run your application you need a `yourapplication.wsgi` file. This file -contains the code `mod_wsgi` is executing on startup to get the application -object. The object called `application` in that file is then used as -application. - -For most applications the following file should be sufficient:: - - from yourapplication import app as application - -If you don't have a factory function for application creation but a singleton -instance you can directly import that one as `application`. - -Store that file somewhere where you will find it again (eg: -`/var/www/yourapplication`) and make sure that `yourapplication` and all -the libraries that are in use are on the python load path. If you don't -want to install it system wide consider using a `virtual python`_ instance. - -Configuring Apache -`````````````````` - -The last thing you have to do is to create an Apache configuration file for -your application. In this example we are telling `mod_wsgi` to execute the -application under a different user for security reasons: - -.. sourcecode:: apache - - - ServerName example.com - - WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 - WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi - - - WSGIProcessGroup yourapplication - WSGIApplicationGroup %{GLOBAL} - Order deny,allow - Allow from all - - - -For more information consult the `mod_wsgi wiki`_. - -.. _mod_wsgi: http://code.google.com/p/modwsgi/ -.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide -.. _virtual python: http://pypi.python.org/pypi/virtualenv -.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ - - -CGI ---- - -If all other deployment methods do not work, CGI will work for sure. CGI -is supported by all major servers but usually has a less-than-optimal -performance. - -This is also the way you can use a Flask application on Google's -`AppEngine`_, there however the execution does happen in a CGI-like -environment. The application's performance is unaffected because of that. - -.. _AppEngine: http://code.google.com/appengine/ - -Creating a `.cgi` file -`````````````````````` - -First you need to create the CGI application file. Let's call it -`yourapplication.cgi`:: - - #!/usr/bin/python - from wsgiref.handlers import CGIHandler - from yourapplication import app - - CGIHandler().run(app) - -If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python -2.5 and higher ship this as part of the standard library. - -Server Setup -```````````` - -Usually there are two ways to configure the server. Either just copy the -`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to -rewrite the URL) or let the server point to the file directly. - -In Apache for example you can put a like like this into the config: - -.. sourcecode:: apache - - ScriptAlias /app /path/to/the/application.cgi - -For more information consult the documentation of your webserver. - - - -FastCGI -------- - -A very popular deployment setup on servers like `lighttpd`_ and `nginx`_ -is FastCGI. To use your WSGI application with any of them you will need -a FastCGI server first. - -The most popular one is `flup`_ which we will use for this guide. Make -sure to have it installed. - -Creating a `.fcgi` file -``````````````````````` - -First you need to create the FastCGI server file. Let's call it -`yourapplication.fcgi`:: - - #!/usr/bin/python - from flup.server.fcgi import WSGIServer - from yourapplication import app - - WSGIServer(app).run() - -This is enough for Apache to work, however lighttpd and nginx need a -socket to communicate with the FastCGI server. For that to work you -need to pass the path to the socket to the -:class:`~flup.server.fcgi.WSGIServer`:: - - WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() - -The path has to be the exact same path you define in the server -config. - -Save the `yourapplication.fcgi` file somewhere you will find it again. -It makes sense to have that in `/var/www/yourapplication` or something -similar. - -Make sure to set the executable bit on that file so that the servers -can execute it:: - - # chmod +x /var/www/yourapplication/yourapplication.fcgi - -Configuring lighttpd -```````````````````` - -A basic FastCGI configuration for lighttpd looks like that:: - - fastcgi.server = ("/yourapplication" => - "yourapplication" => ( - "socket" => "/tmp/yourapplication-fcgi.sock", - "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", - "check-local" => "disable" - ) - ) - -This configuration binds the application to `/yourapplication`. If you -want the application to work in the URL root you have to work around a -lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` -middleware. - -Make sure to apply it only if you are mounting the application the URL -root. - -Configuring nginx -````````````````` - -Installing FastCGI applications on nginx is a bit tricky because by default -some FastCGI parameters are not properly forwarded. - -A basic FastCGI configuration for nginx looks like this:: - - location /yourapplication/ { - include fastcgi_params; - if ($uri ~ ^/yourapplication/(.*)?) { - set $path_url $1; - } - fastcgi_param PATH_INFO $path_url; - fastcgi_param SCRIPT_NAME /yourapplication; - fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; - } - -This configuration binds the application to `/yourapplication`. If you want -to have it in the URL root it's a bit easier because you don't have to figure -out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: - - location /yourapplication/ { - include fastcgi_params; - fastcgi_param PATH_INFO $fastcgi_script_name; - fastcgi_param SCRIPT_NAME ""; - fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; - } - -Since Nginx doesn't load FastCGI apps, you have to do it by yourself. You -can either write an `init.d` script for that or execute it inside a screen -session:: - - $ screen - $ /var/www/yourapplication/yourapplication.fcgi - -Debugging -````````` - -FastCGI deployments tend to be hard to debug on most webservers. Very often the -only thing the server log tells you is something along the lines of "premature -end of headers". In order to debug the application the only thing that can -really give you ideas why it breaks is switching to the correct user and -executing the application by hand. - -This example assumes your application is called `application.fcgi` and that your -webserver user is `www-data`:: - - $ su www-data - $ cd /var/www/yourapplication - $ python application.fcgi - Traceback (most recent call last): - File "yourapplication.fcg", line 4, in - ImportError: No module named yourapplication - -In this case the error seems to be "yourapplication" not being on the python -path. Common problems are: - -- relative paths being used. Don't rely on the current working directory -- the code depending on environment variables that are not set by the - web server. -- different python interpreters being used. - -.. _lighttpd: http://www.lighttpd.net/ -.. _nginx: http://nginx.net/ -.. _flup: http://trac.saddi.com/flup - - - -Tornado --------- - -`Tornado`_ is an open source version of the scalable, non-blocking web server and tools that power `FriendFeed`_. -Because it is non-blocking and uses epoll, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. -Integrating this service with Flask is a trivial task:: - - - from tornado.wsgi import WSGIContainer - from tornado.httpserver import HTTPServer - from tornado.ioloop import IOLoop - from yourapplication import app - - http_server = HTTPServer(WSGIContainer(app)) - http_server.listen(5000) - IOLoop.instance().start() - - -.. _Tornado: http://www.tornadoweb.org/ -.. _FriendFeed: http://friendfeed.com/ - - -Gevent -------- - -`Gevent`_ is a coroutine-based Python networking library that uses `greenlet`_ to provide a high-level synchronous API on top of `libevent`_ event loop:: - - from gevent.wsgi import WSGIServer - from yourapplication import app - - http_server = WSGIServer(('', 5000), app) - http_server.serve_forever() - -.. _Gevent: http://www.gevent.org/ -.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html -.. _libevent: http://monkey.org/~provos/libevent/ diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst new file mode 100644 index 00000000..15b5ff1d --- /dev/null +++ b/docs/deploying/cgi.rst @@ -0,0 +1,42 @@ +CGI +=== + +If all other deployment methods do not work, CGI will work for sure. CGI +is supported by all major servers but usually has a less-than-optimal +performance. + +This is also the way you can use a Flask application on Google's +`AppEngine`_, there however the execution does happen in a CGI-like +environment. The application's performance is unaffected because of that. + +.. _AppEngine: http://code.google.com/appengine/ + +Creating a `.cgi` file +---------------------- + +First you need to create the CGI application file. Let's call it +`yourapplication.cgi`:: + + #!/usr/bin/python + from wsgiref.handlers import CGIHandler + from yourapplication import app + + CGIHandler().run(app) + +If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python +2.5 and higher ship this as part of the standard library. + +Server Setup +------------ + +Usually there are two ways to configure the server. Either just copy the +`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to +rewrite the URL) or let the server point to the file directly. + +In Apache for example you can put a like like this into the config: + +.. sourcecode:: apache + + ScriptAlias /app /path/to/the/application.cgi + +For more information consult the documentation of your webserver. diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst new file mode 100644 index 00000000..b549ddfd --- /dev/null +++ b/docs/deploying/fastcgi.rst @@ -0,0 +1,128 @@ +FastCGI +======= + +A very popular deployment setup on servers like `lighttpd`_ and `nginx`_ +is FastCGI. To use your WSGI application with any of them you will need +a FastCGI server first. + +The most popular one is `flup`_ which we will use for this guide. Make +sure to have it installed. + +Creating a `.fcgi` file +----------------------- + +First you need to create the FastCGI server file. Let's call it +`yourapplication.fcgi`:: + + #!/usr/bin/python + from flup.server.fcgi import WSGIServer + from yourapplication import app + + WSGIServer(app).run() + +This is enough for Apache to work, however lighttpd and nginx need a +socket to communicate with the FastCGI server. For that to work you +need to pass the path to the socket to the +:class:`~flup.server.fcgi.WSGIServer`:: + + WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() + +The path has to be the exact same path you define in the server +config. + +Save the `yourapplication.fcgi` file somewhere you will find it again. +It makes sense to have that in `/var/www/yourapplication` or something +similar. + +Make sure to set the executable bit on that file so that the servers +can execute it:: + + # chmod +x /var/www/yourapplication/yourapplication.fcgi + +Configuring lighttpd +-------------------- + +A basic FastCGI configuration for lighttpd looks like that:: + + fastcgi.server = ("/yourapplication" => + "yourapplication" => ( + "socket" => "/tmp/yourapplication-fcgi.sock", + "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", + "check-local" => "disable" + ) + ) + +This configuration binds the application to `/yourapplication`. If you +want the application to work in the URL root you have to work around a +lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` +middleware. + +Make sure to apply it only if you are mounting the application the URL +root. + +Configuring nginx +----------------- + +Installing FastCGI applications on nginx is a bit tricky because by default +some FastCGI parameters are not properly forwarded. + +A basic FastCGI configuration for nginx looks like this:: + + location /yourapplication/ { + include fastcgi_params; + if ($uri ~ ^/yourapplication/(.*)?) { + set $path_url $1; + } + fastcgi_param PATH_INFO $path_url; + fastcgi_param SCRIPT_NAME /yourapplication; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +This configuration binds the application to `/yourapplication`. If you want +to have it in the URL root it's a bit easier because you don't have to figure +out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: + + location /yourapplication/ { + include fastcgi_params; + fastcgi_param PATH_INFO $fastcgi_script_name; + fastcgi_param SCRIPT_NAME ""; + fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; + } + +Since Nginx doesn't load FastCGI apps, you have to do it by yourself. You +can either write an `init.d` script for that or execute it inside a screen +session:: + + $ screen + $ /var/www/yourapplication/yourapplication.fcgi + +Debugging +--------- + +FastCGI deployments tend to be hard to debug on most webservers. Very often the +only thing the server log tells you is something along the lines of "premature +end of headers". In order to debug the application the only thing that can +really give you ideas why it breaks is switching to the correct user and +executing the application by hand. + +This example assumes your application is called `application.fcgi` and that your +webserver user is `www-data`:: + + $ su www-data + $ cd /var/www/yourapplication + $ python application.fcgi + Traceback (most recent call last): + File "yourapplication.fcg", line 4, in + ImportError: No module named yourapplication + +In this case the error seems to be "yourapplication" not being on the python +path. Common problems are: + +- relative paths being used. Don't rely on the current working directory +- the code depending on environment variables that are not set by the + web server. +- different python interpreters being used. + +.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: http://nginx.net/ +.. _flup: http://trac.saddi.com/flup diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst new file mode 100644 index 00000000..a59e4e9a --- /dev/null +++ b/docs/deploying/index.rst @@ -0,0 +1,19 @@ +Deployment Options +================== + +Depending on what you have available there are multiple ways to run Flask +applications. A very common method is to use the builtin server during +development and maybe behind a proxy for simple applications, but there +are more options available. + +If you have a different WSGI server look up the server documentation about +how to use a WSGI app with it. Just remember that your application object +is the actual WSGI application. + +.. toctree:: + :maxdepth: 2 + + mod_wsgi + cgi + fastcgi + others diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst new file mode 100644 index 00000000..4a2875f2 --- /dev/null +++ b/docs/deploying/mod_wsgi.rst @@ -0,0 +1,80 @@ +mod_wsgi (Apache) +================= + +If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. + +.. _Apache: http://httpd.apache.org/ + +Installing `mod_wsgi` +--------------------- + +If you don't have `mod_wsgi` installed yet you have to either install it using +a package manager or compile it yourself. + +The mod_wsgi `installation instructions`_ cover installation instructions for +source installations on UNIX systems. + +If you are using ubuntu / debian you can apt-get it and activate it as follows:: + + # apt-get install libapache2-mod-wsgi + +On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using +pkg_add:: + + # pkg_add -r mod_wsgi + +If you are using pkgsrc you can install `mod_wsgi` by compiling the +`www/ap2-wsgi` package. + +If you encounter segfaulting child processes after the first apache reload you +can safely ignore them. Just restart the server. + +Creating a `.wsgi` file +----------------------- + +To run your application you need a `yourapplication.wsgi` file. This file +contains the code `mod_wsgi` is executing on startup to get the application +object. The object called `application` in that file is then used as +application. + +For most applications the following file should be sufficient:: + + from yourapplication import app as application + +If you don't have a factory function for application creation but a singleton +instance you can directly import that one as `application`. + +Store that file somewhere where you will find it again (eg: +`/var/www/yourapplication`) and make sure that `yourapplication` and all +the libraries that are in use are on the python load path. If you don't +want to install it system wide consider using a `virtual python`_ instance. + +Configuring Apache +------------------ + +The last thing you have to do is to create an Apache configuration file for +your application. In this example we are telling `mod_wsgi` to execute the +application under a different user for security reasons: + +.. sourcecode:: apache + + + ServerName example.com + + WSGIDaemonProcess yourapplication user=user1 group=group1 threads=5 + WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi + + + WSGIProcessGroup yourapplication + WSGIApplicationGroup %{GLOBAL} + Order deny,allow + Allow from all + + + +For more information consult the `mod_wsgi wiki`_. + +.. _mod_wsgi: http://code.google.com/p/modwsgi/ +.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide +.. _virtual python: http://pypi.python.org/pypi/virtualenv +.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst new file mode 100644 index 00000000..4e2f966c --- /dev/null +++ b/docs/deploying/others.rst @@ -0,0 +1,48 @@ +Other Servers +============= + +There are popular servers written in Python that allow the execution of +WSGI applications as well. Keep in mind though that some of these servers +were written for very specific applications and might not work as well for +standard WSGI application such as Flask powered ones. + + +Tornado +-------- + +`Tornado`_ is an open source version of the scalable, non-blocking web +server and tools that power `FriendFeed`_. Because it is non-blocking and +uses epoll, it can handle thousands of simultaneous standing connections, +which means it is ideal for real-time web services. Integrating this +service with Flask is a trivial task:: + + from tornado.wsgi import WSGIContainer + from tornado.httpserver import HTTPServer + from tornado.ioloop import IOLoop + from yourapplication import app + + http_server = HTTPServer(WSGIContainer(app)) + http_server.listen(5000) + IOLoop.instance().start() + + +.. _Tornado: http://www.tornadoweb.org/ +.. _FriendFeed: http://friendfeed.com/ + + +Gevent +------- + +`Gevent`_ is a coroutine-based Python networking library that uses +`greenlet`_ to provide a high-level synchronous API on top of `libevent`_ +event loop:: + + from gevent.wsgi import WSGIServer + from yourapplication import app + + http_server = WSGIServer(('', 5000), app) + http_server.serve_forever() + +.. _Gevent: http://www.gevent.org/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html +.. _libevent: http://monkey.org/~provos/libevent/ diff --git a/docs/index.rst b/docs/index.rst index 06d8a4e8..5d3ddb2d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,7 +41,7 @@ web development. tutorial/index testing patterns/index - deploying + deploying/index becomingbig design From 6e2be6a0b3aba4ab49cf3ed1a10ac13c6b771aba Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 15:39:07 +0200 Subject: [PATCH 0098/3747] Added JSON Support and started working on jQuery docs --- docs/api.rst | 59 +++++++++++++++++++-- docs/patterns/index.rst | 1 + docs/patterns/jquery.rst | 59 +++++++++++++++++++++ examples/jqueryexample/jqueryexample.py | 29 ++++++++++ examples/jqueryexample/templates/index.html | 25 +++++++++ flask.py | 58 +++++++++++++++++++- tests/flask_tests.py | 32 ++++++++++- 7 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 docs/patterns/jquery.rst create mode 100644 examples/jqueryexample/jqueryexample.py create mode 100644 examples/jqueryexample/templates/index.html diff --git a/docs/api.rst b/docs/api.rst index 5dcdfa95..247294a3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -56,12 +56,15 @@ Incoming Request Data .. attribute:: stream - If the incoming form data was not encoded with a known encoding (for - example it was transmitted as JSON) the data is stored unmodified in - this stream for consumption. For example to read the incoming - request data as JSON, one can do the following:: + If the incoming form data was not encoded with a known mimetype + the data is stored unmodified in this stream for consumption. Most + of the time it is a better idea to use :attr:`data` which will give + you that data as a string. The stream only returns the data once. + + .. attribute:: data - json_body = simplejson.load(request.stream) + Contains the incoming request data as string in case it came with + a mimetype Flask does not handle. .. attribute:: files @@ -106,6 +109,20 @@ Incoming Request Data `root_url` ``http://www.example.com/myapplication/`` ============= ====================================================== + .. attribute:: is_xhr + + `True` if the request was triggered via a JavaScript + `XMLHttpRequest`. This only works with libraries that support the + ``X-Requested-With`` header and set it to `XMLHttpRequest`. + Libraries that do that are prototype, jQuery and Mochikit and + probably some more. + + .. attribute:: json + + Contains the parsed body of the JSON request if the mimetype of + the incoming data was `application/json`. This requires Python 2.6 + or an installed version of simplejson. + Response Objects ---------------- @@ -201,6 +218,38 @@ Message Flashing .. autofunction:: get_flashed_messages +Returning JSON +-------------- + +.. autofunction:: jsonify + +.. data:: json + + If JSON support is picked up, this will be the module that Flask is + using to parse and serialize JSON. So instead of doing this yourself:: + + try: + import simplejson as json + except ImportError: + import json + + You can instead just do this:: + + from flask import json + + For usage examples, read the :mod:`json` documentation. + + The :func:`~json.dumps` function of this json module is also available + as filter called ``|tojson`` in Jinja2. Note that inside `script` + tags no escaping must take place, so make sure to disable escaping + with ``|safe`` if you intend to use it inside `script` tags: + + .. sourcecode:: html+jinja + + + Template Rendering ------------------ diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 037f6e11..8122deb7 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -19,3 +19,4 @@ end of the request, the database connection is closed again. wtforms templateinheritance flashing + jquery diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst new file mode 100644 index 00000000..cb5aa234 --- /dev/null +++ b/docs/patterns/jquery.rst @@ -0,0 +1,59 @@ +AJAX With jQuery +================ + +`jQuery`_ is a small JavaScript library commonly used to simplify working +with the DOM and JavaScript in general. It is the perfect tool to make +web applications more dynamic by exchanging JSON between server and +client. + +.. _jQuery: http://jquery.com/ + +Loading jQuery +-------------- + +In order to use jQuery, you have to download it first and place it in the +static folder of your application and then ensure it's loaded. Ideally +you have a layout template that is used for all pages where you just have +to add two script statements to your `head` section. One for jQuery, and +one for your own script (called `app.js` here): + +.. sourcecode:: html + + + + +Where is My Site? +----------------- + +Do you know where your application is? If you are developing the answer +is quite simple: it's on localhost port something and directly on the root +of that server. But what if you later decide to move your application to +a different location? For example to ``http://example.com/myapp``? On +the server side this never was a problem because we were using the handy +:func:`~flask.url_for` function that did could answer that question for +us, but if we are using jQuery we should better not hardcode the path to +the application but make that dynamic, so how can we do that? + +A simple method would be to add a script tag to our page that sets a +global variable to the prefix to the root of the application. Something +like this: + +.. sourcecode:: html+jinja + + + +The ``|safe`` is necessary so that Jinja does not escape the JSON encoded +string with HTML rules. Usually this would be necessary, but we are +inside a `script` block here where different rules apply. + +.. admonition:: Information for Pros + + In HTML the `script` tag is declared `CDATA` which means that entities + will not be parsed. Everything until ```` is handled as script. + This also means that there must never be any `` +jQuery Example + + + +

    jQuery Example

    +

    + + = + ? +

    calculate server side diff --git a/flask.py b/flask.py index cec1aafb..ba26c7f8 100644 --- a/flask.py +++ b/flask.py @@ -15,11 +15,23 @@ import sys from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware + LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ + cached_property from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException from werkzeug.contrib.securecookie import SecureCookie +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + json_available = False + # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. from werkzeug import abort, redirect @@ -49,6 +61,16 @@ class Request(RequestBase): self.endpoint = None self.view_args = None + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. + """ + if not json_available: + raise AttributeError('simplejson not available') + if self.mimetype == 'application/json': + return json.loads(self.data) + class Response(ResponseBase): """The response object that is used by default in flask. Works like the @@ -81,7 +103,6 @@ class _NullSession(SecureCookie): del _fail - class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the @@ -133,6 +154,8 @@ def get_template_attribute(template_name, attribute): hello = get_template_attribute('_foo.html', 'hello') return hello('World') + .. versionadded:: 0.2 + :param template_name: the name of the template :param attribute: the name of the variable of macro to acccess """ @@ -162,6 +185,35 @@ def get_flashed_messages(): return flashes +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. + + .. versionadded:: 0.2 + """ + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -326,6 +378,8 @@ class Flask(object): url_for=url_for, get_flashed_messages=get_flashed_messages ) + if json_available: + self.jinja_env.filters['tojson'] = json.dumps def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for diff --git a/tests/flask_tests.py b/tests/flask_tests.py index bb560712..0094f657 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -32,7 +32,7 @@ class ContextTestCase(unittest.TestCase): assert meh() == 'http://localhost/meh' -class BasicFunctionality(unittest.TestCase): +class BasicFunctionalityTestCase(unittest.TestCase): def test_request_dispatching(self): app = flask.Flask(__name__) @@ -167,7 +167,35 @@ class BasicFunctionality(unittest.TestCase): == '/static/index.html' -class Templating(unittest.TestCase): +class JSONTestCase(unittest.TestCase): + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data) == d + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + assert rv.data == '3' + + +class TemplatingTestCase(unittest.TestCase): def test_context_processing(self): app = flask.Flask(__name__) From a99e408bd109783561d408ef34c286cdd9b51845 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 15:59:16 +0200 Subject: [PATCH 0099/3747] Finished jQuery example documentation. --- docs/patterns/jquery.rst | 110 ++++++++++++++++++- examples/jqueryexample/templates/index.html | 12 +- examples/jqueryexample/templates/layout.html | 10 ++ 3 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 examples/jqueryexample/templates/layout.html diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index cb5aa234..8d00f54e 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -6,7 +6,16 @@ with the DOM and JavaScript in general. It is the perfect tool to make web applications more dynamic by exchanging JSON between server and client. +JSON itself is a very lightweight transport format, very similar to how +Python primitives (numbers, strings, dicts and lists) look like which is +widely supported and very easy to parse. It became popular a few years +ago and quickly replaced XML as transport format in web applications. + +If you have Python 2.6 JSON will work out of the box, in Python 2.5 you +will have to install the `simplejson`_ library from PyPI. + .. _jQuery: http://jquery.com/ +.. _simplejson: http://pypi.python.org/pypi/simplejson Loading jQuery -------------- @@ -14,15 +23,27 @@ Loading jQuery In order to use jQuery, you have to download it first and place it in the static folder of your application and then ensure it's loaded. Ideally you have a layout template that is used for all pages where you just have -to add two script statements to your `head` section. One for jQuery, and -one for your own script (called `app.js` here): +to add a script statement to your `head` to load jQuery: .. sourcecode:: html - + url_for('static', filename='jquery.js') }}"> + +Another method is using Google's `AJAX Libraries API +`_ to load jQuery: + +.. sourcecode:: html + + + +In this case you don't have to put jQuery into your static folder, it will +instead be loaded from Google directly. This has the advantage that your +website will probably load faster for users if they were to at least one +other website before using the same jQuery version from Google because it +will already be in the browser cache. Downside is that if you don't have +network connectivity during development jQuery will not load. Where is My Site? ----------------- @@ -57,3 +78,82 @@ inside a `script` block here where different rules apply. This also means that there must never be any `` + $(function() { + $('a#calculate').bind('click', function() { + $.getJSON($SCRIPT_ROOT + '/_add_numbers', { + a: $('input[name="a"]').val(), + b: $('input[name="b"]').val() + }, function(data) { + $("#result").text(data.result); + }); + return false; + }); + }); + +

    jQuery Example

    +

    + + = + ? +

    calculate server side + +I won't got into detail here about how jQuery works, just a very quick +explanation of the little bit of code above: + +1. ``$(function() { ... })`` specifies code that should run once the + browser is done loading the basic parts of the page. +2. ``#('selector')`` selects an element and lets you operate on it. +3. ``element.bind('event', func)`` specifies a function that should run + when the user clicked on the element. If that function returns + `false`, the default behaviour will not kick in (in this case, navigate + to the `#` URL). +4. ``$.getJSON(url, data, func)`` sends a `GET` request to `url` and will + send the contents of the `data` object as query parameters. Once the + data arrived, it will call the given function with the return value as + argument. Note that we can use the `$SCRIPT_ROOT` variable here that + we set earlier. + +If you don't get the whole picture, download the `sourcecode +for this example +`_ +from github. diff --git a/examples/jqueryexample/templates/index.html b/examples/jqueryexample/templates/index.html index 6114ae73..0545516d 100644 --- a/examples/jqueryexample/templates/index.html +++ b/examples/jqueryexample/templates/index.html @@ -1,12 +1,6 @@ - -jQuery Example - - +{% extends "layout.html" %} +{% block body %} @@ -23,3 +18,4 @@ = ?

    calculate server side +{% endblock %} diff --git a/examples/jqueryexample/templates/layout.html b/examples/jqueryexample/templates/layout.html new file mode 100644 index 00000000..0b5f3a7e --- /dev/null +++ b/examples/jqueryexample/templates/layout.html @@ -0,0 +1,10 @@ + +jQuery Example + + + +{% block body %}{% endblock %} From f878919753a909e45b2d5448aa175f4f9129ff24 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 16:01:14 +0200 Subject: [PATCH 0100/3747] With -> with in headline --- docs/patterns/jquery.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 8d00f54e..d3d4a7e6 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -1,4 +1,4 @@ -AJAX With jQuery +AJAX with jQuery ================ `jQuery`_ is a small JavaScript library commonly used to simplify working From 3d320176813e87b8dccbbbc807133dfe6ce0bcde Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 17:03:08 +0200 Subject: [PATCH 0101/3747] Added instructions about how to run the latest development version. --- docs/installation.rst | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index c5788be2..bb85b405 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -96,6 +96,44 @@ This is possible as well, but I would not recommend it. Just run (Run it in an Admin shell on Windows systems and without the `sudo`). + +Leaving on the Edge +------------------- + +You want to work with the latest version of Flask, there are two ways: you +can either let `easy_install` pull in the development version or tell it +to operate on a git checkout. Either way it's recommended to do that in a +virtualenv. + +Get the git checkout in a new virtualenv and run in develop mode:: + + $ git clone http://github.com/mitsuhiko/flask.git + Initialized empty Git repository in ~/dev/flask/.git/ + $ cd flask + $ virtualenv env + $ source env/bin/activate + New python executable in env/bin/python + Installing setuptools............done. + $ python setup.py develop + ... + Finished processing dependencies for Flask + +This will pull in the depdenencies and activate the git head as current +version. Then you just have to ``git pull origin`` to get the latest +version. + +To just get the development version without git, do this instead:: + + $ mkdir flask + $ cd flask + $ virtualenv env + $ source env/bin/activate + New python executable in env/bin/python + Installing setuptools............done. + $ easy_install Flask==dev + ... + Finished processing dependencies for Flask==dev + .. _windows-easy-install: `easy_install` on Windows From 07e515b071e32e1b22e9992cedabeb082a8cde6c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 17:05:24 +0200 Subject: [PATCH 0102/3747] Leaving -> living .... --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index bb85b405..c50571e8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -97,8 +97,8 @@ This is possible as well, but I would not recommend it. Just run (Run it in an Admin shell on Windows systems and without the `sudo`). -Leaving on the Edge -------------------- +Living on the Edge +------------------ You want to work with the latest version of Flask, there are two ways: you can either let `easy_install` pull in the development version or tell it From ade490514dd6d83adc7610f69f9c4cd68df47110 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 18:51:04 +0200 Subject: [PATCH 0103/3747] Fixed a security problem caused by changed simplejson semantics. Notice: this was never in a release version of Flask. --- docs/api.rst | 2 ++ docs/patterns/jquery.rst | 3 ++- flask.py | 7 ++++++- tests/flask_tests.py | 6 ++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 247294a3..bd39d69e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -250,6 +250,8 @@ Returning JSON doSomethingWith({{ user.username|tojson|safe }}); + Note that the ``|tojson`` filter escapes forward slashes properly. + Template Rendering ------------------ diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index d3d4a7e6..c12f4474 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -77,7 +77,8 @@ inside a `script` block here where different rules apply. will not be parsed. Everything until ```` is handled as script. This also means that there must never be any ``"|tojson|safe }`` is rendered as + ``"<\/script>"``). JSON View Functions diff --git a/flask.py b/flask.py index ba26c7f8..1dfe8a8f 100644 --- a/flask.py +++ b/flask.py @@ -259,6 +259,11 @@ def _get_package_path(name): return os.getcwd() +def _tojson_filter(string, *args, **kwargs): + """Calls dumps for the template engine, escaping Slashes properly.""" + return json.dumps(string, *args, **kwargs).replace('/', '\\/') + + class Flask(object): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -379,7 +384,7 @@ class Flask(object): get_flashed_messages=get_flashed_messages ) if json_available: - self.jinja_env.filters['tojson'] = json.dumps + self.jinja_env.filters['tojson'] = _tojson_filter def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 0094f657..2d1f85f4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -194,6 +194,12 @@ class JSONTestCase(unittest.TestCase): content_type='application/json') assert rv.data == '3' + def test_template_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.render_template_string('{{ ""|tojson|safe }}') + assert rv == '"<\\/script>"' + class TemplatingTestCase(unittest.TestCase): From 36f105c2932af842de5d18899783f33078415c7f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 18:56:12 +0200 Subject: [PATCH 0104/3747] Removed possible bashism. source -> . --- docs/installation.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index c50571e8..74c5f5e4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -69,7 +69,11 @@ within:: Now you only have to activate it, whenever you work with it. On OS X and Linux do the following:: - $ source env/bin/activate + $ . env/bin/activate + +(Note the whitespace between the dot and the script name. This means +execute this file in context of the shell. If the dot does not work for +whatever reason in your shell, try substituting it with ``source``) If you are a Windows user, the following command is for you:: @@ -111,7 +115,7 @@ Get the git checkout in a new virtualenv and run in develop mode:: Initialized empty Git repository in ~/dev/flask/.git/ $ cd flask $ virtualenv env - $ source env/bin/activate + $ . env/bin/activate New python executable in env/bin/python Installing setuptools............done. $ python setup.py develop @@ -127,7 +131,7 @@ To just get the development version without git, do this instead:: $ mkdir flask $ cd flask $ virtualenv env - $ source env/bin/activate + $ . env/bin/activate New python executable in env/bin/python Installing setuptools............done. $ easy_install Flask==dev From 3088a9371e065808fd0091741f62bc242bcab1f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 20 Apr 2010 00:29:56 +0800 Subject: [PATCH 0105/3747] Make it easier to associate a rule, endpoint and view_function This resolves http://github.com/mitsuhiko/flask/issues/issue/11 --- flask.py | 12 +++++++++--- tests/flask_tests.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/flask.py b/flask.py index 1dfe8a8f..8ed62509 100644 --- a/flask.py +++ b/flask.py @@ -481,9 +481,9 @@ class Flask(object): """ session.save_cookie(response, self.session_cookie_name) - def add_url_rule(self, rule, endpoint, **options): + def add_url_rule(self, rule, endpoint, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` - decorator but does not register the view function for the endpoint. + decorator. If a view_func is provided it will be registered with the endpoint. Basically this example:: @@ -495,19 +495,25 @@ class Flask(object): def index(): pass - app.add_url_rule('index', '/') + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint to a + view function like so: app.view_functions['index'] = index :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as endpoint + :param view_func: the function to call when servicing a request to the provided endpoint :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object """ options['endpoint'] = endpoint options.setdefault('methods', ('GET',)) self.url_map.add(Rule(rule, **options)) + if view_func is not None: + self.view_functions[endpoint] = view_func def route(self, rule, **options): """A decorator that is used to register a view function for a diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2d1f85f4..15166e9a 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -57,6 +57,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + def test_url_mapping(self): + app = flask.Flask(__name__) + def index(): + return flask.request.method + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) + + c = app.test_client() + assert c.get('/').data == 'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD'] + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == 'POST' + assert c.get('/more').data == 'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + def test_session(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From 5acc491c94262912baa30a4cffc780a46b7faf56 Mon Sep 17 00:00:00 2001 From: cgrinds Date: Tue, 20 Apr 2010 01:11:58 +0800 Subject: [PATCH 0106/3747] Refactored the @route decorator to use the new add_url_rule method. Tagged add_url_rule with versionadded of 0.2 --- flask.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask.py b/flask.py index 8ed62509..c5a969ca 100644 --- a/flask.py +++ b/flask.py @@ -508,6 +508,8 @@ class Flask(object): :param view_func: the function to call when servicing a request to the provided endpoint :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object + + .. versionadded:: 0.2 """ options['endpoint'] = endpoint options.setdefault('methods', ('GET',)) @@ -581,8 +583,7 @@ class Flask(object): :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - self.add_url_rule(rule, f.__name__, **options) - self.view_functions[f.__name__] = f + self.add_url_rule(rule, f.__name__, f, **options) return f return decorator From 95750b3287bfa21fbb706f10a3a9fbcef01c089e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 23:49:02 +0200 Subject: [PATCH 0107/3747] Documented some routing converter possibilities. --- docs/api.rst | 2 +- flask.py | 23 +++++++++++++++++++---- tests/flask_tests.py | 17 ++++++++++++++++- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index bd39d69e..122da451 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -106,7 +106,7 @@ Incoming Request Data `script_root` ``/myapplication`` `url` ``http://www.example.com/myapplication/page.html`` `base_url` ``http://www.example.com/myapplication/page.html?x=y`` - `root_url` ``http://www.example.com/myapplication/`` + `url_root` ``http://www.example.com/myapplication/`` ============= ====================================================== .. attribute:: is_xhr diff --git a/flask.py b/flask.py index c5a969ca..055b46cf 100644 --- a/flask.py +++ b/flask.py @@ -16,7 +16,7 @@ import sys from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - cached_property + ImmutableDict, cached_property from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException from werkzeug.contrib.securecookie import SecureCookie @@ -306,7 +306,7 @@ class Flask(object): session_cookie_name = 'session' #: options that are passed directly to the Jinja2 environment - jinja_options = dict( + jinja_options = ImmutableDict( autoescape=True, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -361,11 +361,26 @@ class Flask(object): #: decorator. self.template_context_processors = [_default_template_ctx_processor] + #: the :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(BaseConverter.to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter self.url_map = Map() if self.static_path is not None: - self.url_map.add(Rule(self.static_path + '/', - build_only=True, endpoint='static')) + self.add_url_rule(self.static_path + '/', + build_only=True, endpoint='static') if pkg_resources is not None: target = (self.package_name, 'static') else: diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 15166e9a..60a6b7df 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -63,7 +63,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): return flask.request.method def more(): return flask.request.method - + app.add_url_rule('/', 'index', index) app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) @@ -181,6 +181,21 @@ class BasicFunctionalityTestCase(unittest.TestCase): with app.test_request_context(): assert flask.url_for('hello', name='test x') == '/hello/test%20x' + def test_custom_converters(self): + from werkzeug.routing import BaseConverter + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(',') + def to_url(self, value): + return ','.join(super(ListConverter, self).to_url(x) for x in value) + app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter + @app.route('/') + def index(args): + return '|'.join(args) + c = app.test_client() + assert c.get('/1,2,3').data == '1|2|3' + def test_static_files(self): app = flask.Flask(__name__) rv = app.test_client().get('/static/index.html') From 06ec917ddedc618944670b39ccd5f4cb862882b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Apr 2010 23:54:38 +0200 Subject: [PATCH 0108/3747] Only escape backslashes if they follow "<" in JSON dumping for templates. --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 055b46cf..89a4fee5 100644 --- a/flask.py +++ b/flask.py @@ -261,7 +261,7 @@ def _get_package_path(name): def _tojson_filter(string, *args, **kwargs): """Calls dumps for the template engine, escaping Slashes properly.""" - return json.dumps(string, *args, **kwargs).replace('/', '\\/') + return json.dumps(string, *args, **kwargs).replace(' Date: Tue, 20 Apr 2010 01:35:22 +0200 Subject: [PATCH 0109/3747] Fixed typo in form docs --- docs/patterns/wtforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 2a0b4ac8..b94b4156 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -23,7 +23,7 @@ This is an example form for a typical registration page:: username = TextField('Username', [validators.Length(min=4, max=25)]) email = TextField('Email Address', [validators.Length(min=6, max=35)]) password = PasswordField('New Password', [Required(), - EqualTo('confirm', mesage='Passwords must match')]) + EqualTo('confirm', message='Passwords must match')]) confirm = PasswordField('Repeat Password') accept_tos = BooleanField('I accept the TOS', [validators.Required()]) From 4ff9493e5765f3d2c2fd2dd1882791a75ad6fc68 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Apr 2010 01:37:51 +0200 Subject: [PATCH 0110/3747] POST -> post in HTML --- docs/patterns/wtforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index b94b4156..29dc65d1 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -95,7 +95,7 @@ takes advantage of the `_formhelpers.html` template: .. sourcecode:: html+jinja {% from "_formhelpers.html" import render_field %} -

    +
    {{ render_field(form.username) }} {{ render_field(form.email) }} From a7ff9dbddd5f861463051601177442e98aae84bf Mon Sep 17 00:00:00 2001 From: Chris Edgemon Date: Mon, 19 Apr 2010 23:25:51 -0500 Subject: [PATCH 0111/3747] Proofreading the documentation --- docs/becomingbig.rst | 18 +++++++++--------- docs/deploying/mod_wsgi.rst | 6 +++--- docs/design.rst | 22 +++++++++++----------- docs/installation.rst | 18 +++++++++--------- docs/patterns/packages.rst | 6 +++--- docs/patterns/sqlalchemy.rst | 18 +++++++++--------- docs/patterns/sqlite3.rst | 6 +++--- docs/testing.rst | 22 +++++++++++----------- 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index c3c4a0b6..02344720 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -8,11 +8,11 @@ designed for large scale applications and does not attempt to do so, but that does not mean you picked the wrong tool in the first place. Flask is powered by Werkzeug and Jinja2, two libraries that are in use at -a number of large websites out there and all Flask does is bringing those +a number of large websites out there and all Flask does is bring those two together. Being a microframework, Flask is literally a single file. What that means for large applications is that it's probably a good idea to take the code from Flask and put it into a new module within the -applications and expanding on that. +applications and expand on that. What Could Be Improved? ----------------------- @@ -20,12 +20,12 @@ What Could Be Improved? For instance it makes a lot of sense to change the way endpoints (the names of the functions / URL rules) are handled to also take the module name into account. Right now the function name is the URL name, but -imagine you have a large applications consisting of multiple components. +imagine you have a large application consisting of multiple components. In that case, it makes a lot of sense to use dotted names for the URL endpoints. -Here some suggestions how Flask can be modified to better accomodate large -scale applications: +Here are some suggestions for how Flask can be modified to better +accomodate large-scale applications: - implement dotted names for URL endpoints - get rid of the decorator function registering which causes a lot @@ -35,7 +35,7 @@ scale applications: better solution would be to have one module with all URLs in there and specifing the target functions explicitly or by name and importing them when needed. -- switch to explicit request object passing. This makes it more to type +- switch to explicit request object passing. This requires more typing (because you now have something to pass around) but it makes it a whole lot easier to debug hairy situations and to test the code. - integrate the `Babel`_ i18n package or `SQLAlchemy`_ directly into the @@ -44,14 +44,14 @@ scale applications: .. _Babel: http://babel.edgewall.org/ .. _SQLAlchemy: http://www.sqlalchemy.org/ -Why does not Flask do all that by Default? +Why does Flask not do all that by Default? ------------------------------------------ There is a huge difference between a small application that only has to handle a couple of requests per second and with an overall code complexity -of less than 4000 lines of code or something of larger scale. At one +of less than 4000 lines of code and something of larger scale. At some point it becomes important to integrate external systems, different storage backends and more. If Flask was designed with all these contingencies in mind, it would be a -much more complex framework and less easy to get started with. +much more complex framework and harder to get started with. diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 4a2875f2..a7bbc114 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -11,8 +11,8 @@ Installing `mod_wsgi` If you don't have `mod_wsgi` installed yet you have to either install it using a package manager or compile it yourself. -The mod_wsgi `installation instructions`_ cover installation instructions for -source installations on UNIX systems. +The mod_wsgi `installation instructions`_ cover source installations on UNIX +systems. If you are using ubuntu / debian you can apt-get it and activate it as follows:: @@ -44,7 +44,7 @@ For most applications the following file should be sufficient:: If you don't have a factory function for application creation but a singleton instance you can directly import that one as `application`. -Store that file somewhere where you will find it again (eg: +Store that file somewhere that you will find it again (e.g.: `/var/www/yourapplication`) and make sure that `yourapplication` and all the libraries that are in use are on the python load path. If you don't want to install it system wide consider using a `virtual python`_ instance. diff --git a/docs/design.rst b/docs/design.rst index ae1fd8d0..c4fd32dd 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -2,7 +2,7 @@ Design Decisions in Flask ========================= If you are curious why Flask does certain things the way it does and not -different, this section is for you. This should give you an idea about +differently, this section is for you. This should give you an idea about some of the design decisions that may appear arbitrary and surprising at first, especially in direct comparison with other frameworks. @@ -44,10 +44,10 @@ something it can be very helpful to create a minimal application to test specific behavior. When the application object is deleted everything it allocated will be freed again. -Another thing that becomes possible with having an explicit object laying +Another thing that becomes possible when you have an explicit object laying around in your code is that you can subclass the base class (:class:`~flask.Flask`) to alter specific behaviour. This would not be -possible without hacks if the object was created ahead of time for you +possible without hacks if the object were created ahead of time for you based on a class that is not exposed to you. But there is another very important reason why Flask depends on an @@ -83,23 +83,23 @@ that limitation that Jinja2 is *always* configured will probably go away, the decision to bundle one template engine and use that will not. Template engines are like programming languages and each of those engines -has a certain understandment about how things work. On the surface they +has a certain understanding about how things work. On the surface they all work the same: you tell the engine to evaluate a template with a set of variables and take the return value as string. But that's about where similarities end. Jinja2 for example has an extensive filter system, a certain way to do template inheritance, support for reusable blocks (macros) that can be used from inside templates and -also from Python code, is using unicode for all operations, supports +also from Python code, uses unicode for all operations, supports iterative template rendering, configurable syntax and more. On the other hand an engine like Genshi is based on XML stream evaluation, template inheritance by taking the availability of XPath into account and more. Mako on the other hand treats templates similar to Python modules. -When it comes to bridge a template engine with an application or framework -there is more than just rendering templates. Flask uses Jinja2's -extensive autoescaping support for instance. Also it provides ways to -access macros from Jinja2 templates. +When it comes to connecting a template engine with an application or +framework there is more than just rendering templates. For instance, +Flask uses Jinja2's extensive autoescaping support. Also it provides +ways to access macros from Jinja2 templates. A template abstraction layer that would not take the unique features of the template engines away is a science on its own and a too large @@ -115,9 +115,9 @@ over to the Ruby side of web development there we have a protocol very similar to WSGI. Just that it's called Rack there, but besides that it looks very much like a WSGI rendition for Ruby. But nearly all applications in Ruby land do not work with Rack directly, but on top of a -lirbary with the same name. This Rack library has two equivalents in +library with the same name. This Rack library has two equivalents in Python: WebOb (formerly Paste) and Werkzeug. Paste is still around but -from my understanding it's sortof deprecated in favour of WebOb. The +from my understanding it's sort of deprecated in favour of WebOb. The development of WebOb and Werkzeug started side by side with similar ideas in mind: be a good implementation of WSGI for other applications to take advantage. diff --git a/docs/installation.rst b/docs/installation.rst index 74c5f5e4..f2f4905f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,10 +9,10 @@ way and why there are multiple ways. Flask depends on two external libraries: `Werkzeug `_ and `Jinja2 `_. -The first on is responsible for interfacing WSGI the latter to render +The first one is responsible for interfacing WSGI the latter for rendering templates. Now you are maybe asking, what is WSGI? WSGI is a standard in Python that is basically responsible for ensuring that your application -is behaving in a specific way that you can run it on different +is behaving in a specific way so that you can run it on different environments (for example on a local development server, on an Apache2, on lighttpd, on Google's App Engine or whatever you have in mind). @@ -26,10 +26,10 @@ Virtualenv is what you want to use during development and in production if you have shell access. So first: what does virtualenv do? If you are like me and you like Python, chances are you want to use it for another project as well. Now the more projects you have, the more likely it is -that you will be working with different versions of Python itself or a -library involved. Because let's face it: quite often libraries break -backwards compatibility and it's unlikely that your application will -not have any dependencies, that just won't happen. So virtualenv for the +that you will be working with different versions of Python itself or at +least an individual library. Because let's face it: quite often libraries +break backwards compatibility and it's unlikely that your application will +not have any dependencies, that just won't happen. So virtualenv to the rescue! It basically makes it possible to have multiple side-by-side @@ -47,7 +47,7 @@ or even better:: $ sudo pip install virtualenv -Changes are you have virtualenv installed on your system then. Maybe it's +Chances are you have virtualenv installed on your system then. Maybe it's even in your package manager (on ubuntu try ``sudo apt-get install python-virtualenv``). @@ -152,7 +152,7 @@ Once you have done that it's important to add the `easy_install` command and other Python scripts to the path. To do that you have to add the Python installation's Script folder to the `PATH` variable. -To do that, click right on your "Computer" desktop icon and click +To do that, right-click on your "Computer" desktop icon and click "Properties". On Windows Vista and Windows 7 then click on "Advanced System settings", on Windows XP click on the "Advanced" tab instead. Then click on the "Environment variables" button and double click on the "Path" @@ -165,7 +165,7 @@ the following value:: ;C:\Python26\Scripts -Then you are done. To check if it worked, open the cmd and execute +Then you are done. To check that it worked, open the cmd and execute "easy_install". If you have UAC enabled it should prompt you for admin privileges. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index d0b28932..5e14625b 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -48,12 +48,12 @@ What did we gain from this? Now we can restructure the application a bit into multiple modules. The only thing you have to remember is the following quick checklist: -1. the `Flask` application object creation have to be in the +1. the `Flask` application object creation has to be in the `__init__.py` file. That way each module can import it safely and the - `__name__` variable will resole to the correct package. + `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` decorator on top) have to be imported when in the `__init__.py` file. - Not the objects itself, but the module it is in. Do the importing at + Not the object itself, but the module it is in. Do the importing at the *bottom* of the file. Here an example `__init__.py`:: diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 5a064469..32d41c08 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -6,7 +6,7 @@ SQLAlchemy in Flask Many people prefer `SQLAlchemy`_ for database access. In this case it's encouraged to use a package instead of a module for your flask application and drop the models into a separate module (:ref:`larger-applications`). -Although that is not necessary but makes a lot of sense. +While that is not necessary, it makes a lot of sense. There are three very common ways to use SQLAlchemy. I will outline each of them here: @@ -52,7 +52,7 @@ automatically remove database sessions at the end of the request for you:: db_session.remove() return response -Here an example model (put that into `models.py` for instance):: +Here is an example model (put this into `models.py`, e.g.):: from sqlalchemy import Column, Integer, String from yourapplication.database import Base @@ -70,7 +70,7 @@ Here an example model (put that into `models.py` for instance):: def __repr__(self): return '' % (self.name, self.email) -You can insert entries into the database like this then: +You can insert entries into the database like this: >>> from yourapplication.database import db_session >>> from yourapplication.models import User @@ -95,11 +95,11 @@ Manual Object Relational Mapping Manual object relational mapping has a few upsides and a few downsides versus the declarative approach from above. The main difference is that you define tables and classes separately and map them together. It's more -flexible but a little more to type. In general it works similar to the +flexible but a little more to type. In general it works like the declarative approach, so make sure to also split up your application into multiple modules in a package. -Here the example `database.py` module for your application:: +Here is an example `database.py` module for your application:: from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker @@ -112,7 +112,7 @@ Here the example `database.py` module for your application:: def init_db(): metadata.create_all(bind=engine) -As for the declarative approach you need to close down the session after +As for the declarative approach you need to close the session after each request. Put this into your application module:: from yourapplication.database import db_session @@ -122,7 +122,7 @@ each request. Put this into your application module:: db_session.remove() return response -Here an example table and model (put that into `models.py` for instance):: +Here is an example table and model (put this into `models.py`):: from sqlalchemy import Table, Column, Integer, String from sqlalchemy.orm import mapper @@ -172,7 +172,7 @@ connection first so that we can use a transaction: SQLAlchemy will automatically commit for us. -To query your database, yu use the engine directly or use a connection: +To query your database, you use the engine directly or use a connection: >>> users.select(users.c.id == 1).execute().first() (1, u'admin', u'admin@localhost') @@ -183,7 +183,7 @@ These results are also dict-like tuples: >>> r['name'] u'admin' -You can also pass string of SQL statements to the +You can also pass strings of SQL statements to the :meth:`~sqlalchemy.engine.base.Connection.execute` method: >>> engine.execute('select * from users where id = :1', [1]).first() diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 3e758002..c11e837d 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,12 +3,12 @@ Using SQLite 3 with Flask ========================= -In Flask you can implement opening of dabase connections at the beginning +In Flask you can implement opening of database connections at the beginning of the request and closing at the end with the :meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` decorators in combination with the special :class:`~flask.g` object. -So here a simple example how you can use SQLite 3 with Flask:: +So here a simple example of how you can use SQLite 3 with Flask:: import sqlite3 from flask import g @@ -70,7 +70,7 @@ Initial Schemas Relational databases need schemas, so applications often ship a `schema.sql` file that creates the database. It's a good idea to provide -a function that creates the database bases on that schema. This function +a function that creates the database based on that schema. This function can do that for you:: from contextlib import closing diff --git a/docs/testing.rst b/docs/testing.rst index 5439fbaf..3ea1e9ba 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -8,14 +8,14 @@ Testing Flask Applications Not sure where that is coming from, and it's not entirely correct, but also not that far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to -become pretty paranoid. If an application however has automated tests you -can savely change things and you will instantly know if your change broke +become pretty paranoid. If an application however has automated tests, you +can safely change things and you will instantly know if your change broke something. Flask gives you a couple of ways to test applications. It mainly does that by exposing the Werkzeug test :class:`~werkzeug.Client` class to your code and handling the context locals for you. You can then use that with -your favourite testing solution. In this documentation we will us the +your favourite testing solution. In this documentation we will use the :mod:`unittest` package that comes preinstalled with each Python installation. @@ -50,13 +50,13 @@ In order to test that, we add a second module ( if __name__ == '__main__': unittest.main() -The code in the `setUp` function creates a new test client and initialize +The code in the `setUp` function creates a new test client and initializes a new database. That function is called before each individual test function. -What the test client does for us is giving us a simple interface to the +What the test client does is give us a simple interface to the application. We can trigger test requests to the application and the client will also keep track of cookies for us. -Because SQLite3 is filesystem based we can easily use the tempfile module +Because SQLite3 is filesystem-based we can easily use the tempfile module to create a temporary database and initialize it. Just make sure that you keep a reference to the :class:`~tempfile.NamedTemporaryFile` around (we store it as `self.db` because of that) so that the garbage collector does @@ -112,20 +112,20 @@ Run it again and you should see one passing test:: OK -Of course you can submit forms with the test client as well which we will +Of course you can submit forms with the test client as well, which we will use now to log our user in. Logging In and Out ------------------ The majority of the functionality of our application is only available for -the administration user. So we need a way to log our test client into the +the administration user. So we need a way to log our test client in to the application and out of it again. For that we fire some requests to the login and logout pages with the required form data (username and password). Because the login and logout pages redirect, we tell the client to `follow_redirects`. -Add the following two methods do your `FlaskrTestCase` class:: +Add the following two methods to your `FlaskrTestCase` class:: def login(self, username, password): return self.app.post('/login', data=dict( @@ -137,7 +137,7 @@ Add the following two methods do your `FlaskrTestCase` class:: return self.app.get('/logout', follow_redirects=True) Now we can easily test if logging in and out works and that it fails with -invalid credentials. Add this as new test to the class:: +invalid credentials. Add this new test to the class:: def test_login_logout(self): rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) @@ -165,7 +165,7 @@ like this:: assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data -Here we also check that HTML is allowed in the text but not in the title +Here we check that HTML is allowed in the text but not in the title, which is the intended behavior. Running that should now give us three passing tests:: From 9f0b2429d638e54cb534b555c393d0770dfc1a8f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Apr 2010 10:51:36 +0200 Subject: [PATCH 0112/3747] The testsuite skips JSON tests now is not available and runs the example tests as well. --- tests/flask_tests.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 60a6b7df..f5f7dfdc 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -10,11 +10,18 @@ :license: BSD, see LICENSE for more details. """ from __future__ import with_statement +import os +import sys import flask import unittest import tempfile +example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') +sys.path.append(os.path.join(example_path, 'flaskr')) +sys.path.append(os.path.join(example_path, 'minitwit')) + + class ContextTestCase(unittest.TestCase): def test_context_binding(self): @@ -277,5 +284,19 @@ class TemplatingTestCase(unittest.TestCase): assert macro('World') == 'Hello World!' +def suite(): + from minitwit_tests import MiniTwitTestCase + from flaskr_tests import FlaskrTestCase + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ContextTestCase)) + suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) + suite.addTest(unittest.makeSuite(TemplatingTestCase)) + if flask.json_available: + suite.addTest(unittest.makeSuite(JSONTestCase)) + suite.addTest(unittest.makeSuite(MiniTwitTestCase)) + suite.addTest(unittest.makeSuite(FlaskrTestCase)) + return suite + + if __name__ == '__main__': - unittest.main() + unittest.main(defaultTest='suite') From 1404a85bc554f96bff5463e6eadb14662b945f2e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Apr 2010 11:22:20 +0200 Subject: [PATCH 0113/3747] Fixed WTForms example. --- docs/patterns/wtforms.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 29dc65d1..4a836975 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -22,9 +22,9 @@ This is an example form for a typical registration page:: class RegistrationForm(Form): username = TextField('Username', [validators.Length(min=4, max=25)]) email = TextField('Email Address', [validators.Length(min=6, max=35)]) - password = PasswordField('New Password', [Required(), - EqualTo('confirm', message='Passwords must match')]) - confirm = PasswordField('Repeat Password') + password = PasswordField('New Password', [validators.Required()]) + confirm = PasswordField('Repeat Password', [validators.EqualTo( + 'confirm', message='Passwords must match')]) accept_tos = BooleanField('I accept the TOS', [validators.Required()]) In the View From 715f64d2afef0e5b9800f0e0fb0eb9c892e1573a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Apr 2010 13:01:10 +0200 Subject: [PATCH 0114/3747] Some more documentation updates. --- docs/patterns/jquery.rst | 9 ++++++++- flask.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index c12f4474..32eb6f04 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -88,7 +88,7 @@ Now let's create a server side function that accepts two URL arguments of numbers which should be added together and then sent back to the application in a JSON object. This is a really ridiculous example and is something you usually would do on the client side alone, but a simple -example that shows how you would use jQuer and Flask nonetheless:: +example that shows how you would use jQuery and Flask nonetheless:: from flask import Flask, jsonify, render_template, request app = Flask(__name__) @@ -108,6 +108,13 @@ template. This template will load jQuery as above and have a little form we can add two numbers and a link to trigger the function on the server side. +Note that we are using the :meth:`~werkzeug.MultiDict.get` method here +which will never fail. If the key is missing a default value (here ``0``) +is returned. Furthermore it can convert values to a specific type (like +in our case `int`). This is especially handy for code that that is +triggered by a script (APIs, JavaScript etc.) because you don't need +special error reporting in that case. + The HTML -------- diff --git a/flask.py b/flask.py index 89a4fee5..bc99bee0 100644 --- a/flask.py +++ b/flask.py @@ -66,8 +66,8 @@ class Request(RequestBase): """If the mimetype is `application/json` this will contain the parsed JSON data. """ - if not json_available: - raise AttributeError('simplejson not available') + if __debug__: + _assert_have_json() if self.mimetype == 'application/json': return json.loads(self.data) @@ -210,6 +210,8 @@ def jsonify(*args, **kwargs): .. versionadded:: 0.2 """ + if __debug__: + _assert_have_json() return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') @@ -251,6 +253,12 @@ def _default_template_ctx_processor(): ) +def _assert_have_json(): + """Helper function that fails if JSON is unavailable""" + if not json_available: + raise RuntimeError('simplejson not installed') + + def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: @@ -261,6 +269,8 @@ def _get_package_path(name): def _tojson_filter(string, *args, **kwargs): """Calls dumps for the template engine, escaping Slashes properly.""" + if __debug__: + _assert_have_json() return json.dumps(string, *args, **kwargs).replace(' Date: Tue, 20 Apr 2010 13:29:54 +0200 Subject: [PATCH 0115/3747] Fixed typo and added AUTHORS file and license text to docs. --- AUTHORS | 13 +++++++++++++ LICENSE | 21 +++++++++++---------- docs/index.rst | 1 + docs/license.rst | 21 +++++++++++++++++++++ flask.py | 2 +- 5 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 AUTHORS create mode 100644 docs/license.rst diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..92379c48 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,13 @@ +Flask is written and maintained by Armin Ronacher and +various contributors: + +Development Lead +```````````````` + +- Armin Ronacher + +Patches and Suggestions +``````````````````````` + +- Chris Edgemon +- Chris Grindstaff diff --git a/LICENSE b/LICENSE index 6ed65ec8..6a8df19e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2010 by Armin Ronacher. +Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS +for more details. Some rights reserved. @@ -6,17 +7,17 @@ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. - * The names of the contributors may not be used to endorse or - promote products derived from this software without specific - prior written permission. +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT diff --git a/docs/index.rst b/docs/index.rst index 5d3ddb2d..9b2f132b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ web development. deploying/index becomingbig design + license Reference --------- diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..918a75b1 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,21 @@ +License +======= + +Flask is licensed under a three clause `BSD License`_. It basically +means: do whatever you want with it as long as the copyright in Flask +sticks around, the conditions are not modified and the disclaimer is +present. Furthermore you must not use the names of the authors to promote +derivates of the software without written consent. + +.. _BSD License: + http://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22New_BSD_License.22.29 + +Authors +------- + +.. include:: ../AUTHORS + +License Text +------------ + +.. include:: ../LICENSE diff --git a/flask.py b/flask.py index bc99bee0..05197c81 100644 --- a/flask.py +++ b/flask.py @@ -358,7 +358,7 @@ class Flask(object): self.before_request_funcs = [] #: a list of functions that are called at the end of the - #: request. Tha function is passed the current response + #: request. The function is passed the current response #: object and modify it in place or replace it. #: To register a function here use the :meth:`after_request` #: decorator. From 268302fc68c35abd555a8c2d2755f14c96e1401d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Apr 2010 14:22:34 +0200 Subject: [PATCH 0116/3747] Changed link to license. --- website/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/index.html b/website/index.html index ae835ac2..5db6b850 100644 --- a/website/index.html +++ b/website/index.html @@ -26,7 +26,7 @@

    because sometimes a pocket knife is not enough

    Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions. - And before you ask: It's BSD licensed! + And before you ask: It's BSD licensed!

    Flask is Fun

    from flask import Flask
    
    From 9f6bc93e4de36fc209eb1203845e9505df1ca379 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 15:12:16 +0200
    Subject: [PATCH 0117/3747] Fixed XSS problem by escaping all slashes in JSON.
    
    This also probes simplejson first to figure out if it escapes slashes
    which it did in earlier versions.
    ---
     flask.py             | 12 +++++++++++-
     tests/flask_tests.py |  2 ++
     2 files changed, 13 insertions(+), 1 deletion(-)
    
    diff --git a/flask.py b/flask.py
    index 05197c81..cb638e70 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -10,6 +10,7 @@
         :license: BSD, see LICENSE for more details.
     """
     from __future__ import with_statement
    +import re
     import os
     import sys
     
    @@ -47,6 +48,12 @@ except (ImportError, AttributeError):
         pkg_resources = None
     
     
    +# figure out if simplejson escapes slashes.  This behaviour was changed
    +# from one version to another without reason.
    +if json_available:
    +    _json_escapes_slashes = '\\/' in json.dumps('/')
    +
    +
     class Request(RequestBase):
         """The request object used by default in flask.  Remembers the
         matched endpoint and view arguments.
    @@ -271,7 +278,10 @@ def _tojson_filter(string, *args, **kwargs):
         """Calls dumps for the template engine, escaping Slashes properly."""
         if __debug__:
             _assert_have_json()
    -    return json.dumps(string, *args, **kwargs).replace('"|tojson|safe }}')
                 assert rv == '"<\\/script>"'
    +            rv = flask.render_template_string('{{ "<\0/script>"|tojson|safe }}')
    +            assert rv == '"<\\u0000\\/script>"'
     
     
     class TemplatingTestCase(unittest.TestCase):
    
    From 3c821a0fa45082bcec73dfb08662fb1de5263c48 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Tue, 20 Apr 2010 18:40:58 +0200
    Subject: [PATCH 0118/3747] Fix typos and remove unused import.
    
    ---
     docs/api.rst                  | 2 +-
     docs/patterns/jquery.rst      | 2 +-
     docs/patterns/packages.rst    | 5 ++---
     examples/minitwit/minitwit.py | 5 +++--
     flask.py                      | 9 ++++-----
     setup.py                      | 6 ++++--
     6 files changed, 15 insertions(+), 14 deletions(-)
    
    diff --git a/docs/api.rst b/docs/api.rst
    index 122da451..d285dbfd 100644
    --- a/docs/api.rst
    +++ b/docs/api.rst
    @@ -186,7 +186,7 @@ different values for each request.  In a nutshell: it does the right
     thing, like it does for :class:`request` and :class:`session`.
     
     .. data:: g
    -   
    +
        Just store on this whatever you want.  For example a database
        connection or the user that is currently logged in.
     
    diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst
    index 32eb6f04..f087e6f0 100644
    --- a/docs/patterns/jquery.rst
    +++ b/docs/patterns/jquery.rst
    @@ -111,7 +111,7 @@ side.
     Note that we are using the :meth:`~werkzeug.MultiDict.get` method here
     which will never fail.  If the key is missing a default value (here ``0``)
     is returned.  Furthermore it can convert values to a specific type (like
    -in our case `int`).  This is especially handy for code that that is
    +in our case `int`).  This is especially handy for code that is
     triggered by a script (APIs, JavaScript etc.) because you don't need
     special error reporting in that case.
     
    diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst
    index 5e14625b..4d54e49c 100644
    --- a/docs/patterns/packages.rst
    +++ b/docs/patterns/packages.rst
    @@ -74,10 +74,9 @@ And this is what `views.py` would look like::
     .. admonition:: Circular Imports
     
        Every Python programmer hates them, and yet we just added some:
    -   circular imports (That's when two module depend on each one.  In this
    +   circular imports (That's when two modules depend on each other.  In this
        case `views.py` depends on `__init__.py`).  Be advised that this is a
    -   bad idea in general but here it is actually fine.  The reason for this
    -   is
    +   bad idea in general but here it is actually fine.  The reason for this is
        that we are not actually using the views in `__init__.py` and just
        ensuring the module is imported and we are doing that at the bottom of
        the file.
    diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py
    index ef0c7673..07ffe4c7 100644
    --- a/examples/minitwit/minitwit.py
    +++ b/examples/minitwit/minitwit.py
    @@ -125,7 +125,8 @@ def user_timeline(username):
         if g.user:
             followed = query_db('''select 1 from follower where
                 follower.who_id = ? and follower.whom_id = ?''',
    -            [session['user_id'], profile_user['user_id']], one=True) is not None
    +            [session['user_id'], profile_user['user_id']],
    +            one=True) is not None
         return render_template('timeline.html', messages=query_db('''
                 select message.*, user.* from message, user where
                 user.user_id = message.author_id and user.user_id = ?
    @@ -230,7 +231,7 @@ def register():
     
     @app.route('/logout')
     def logout():
    -    """Logs the user out"""
    +    """Logs the user out."""
         flash('You were logged out')
         session.pop('user_id', None)
         return redirect(url_for('public_timeline'))
    diff --git a/flask.py b/flask.py
    index cb638e70..e84519cd 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -10,7 +10,6 @@
         :license: BSD, see LICENSE for more details.
     """
     from __future__ import with_statement
    -import re
     import os
     import sys
     
    @@ -261,7 +260,7 @@ def _default_template_ctx_processor():
     
     
     def _assert_have_json():
    -    """Helper function that fails if JSON is unavailable"""
    +    """Helper function that fails if JSON is unavailable."""
         if not json_available:
             raise RuntimeError('simplejson not installed')
     
    @@ -517,7 +516,7 @@ class Flask(object):
     
         def add_url_rule(self, rule, endpoint, view_func=None, **options):
             """Connects a URL rule.  Works exactly like the :meth:`route`
    -        decorator. If a view_func is provided it will be registered with the
    +        decorator.  If a view_func is provided it will be registered with the
             endpoint.
     
             Basically this example::
    @@ -544,7 +543,7 @@ class Flask(object):
             :param endpoint: the endpoint for the registered URL rule.  Flask
                              itself assumes the name of the view function as
                              endpoint
    -        :param view_func: the function to call when servicing a request to the
    +        :param view_func: the function to call when serving a request to the
                               provided endpoint
             :param options: the options to be forwarded to the underlying
                             :class:`~werkzeug.routing.Rule` object
    @@ -798,7 +797,7 @@ class Flask(object):
             return self.request_context(create_environ(*args, **kwargs))
     
         def __call__(self, environ, start_response):
    -        """Shortcut for :attr:`wsgi_app`"""
    +        """Shortcut for :attr:`wsgi_app`."""
             return self.wsgi_app(environ, start_response)
     
     
    diff --git a/setup.py b/setup.py
    index 1742542d..ae68c49e 100644
    --- a/setup.py
    +++ b/setup.py
    @@ -34,7 +34,8 @@ Links
     
     * `website `_
     * `documentation `_
    -* `development version `_
    +* `development version
    +  `_
     
     """
     from setuptools import setup
    @@ -47,7 +48,8 @@ setup(
         license='BSD',
         author='Armin Ronacher',
         author_email='armin.ronacher@active-4.com',
    -    description='A microframework based on Werkzeug, Jinja2 and good intentions',
    +    description='A microframework based on Werkzeug, Jinja2 '
    +                'and good intentions',
         long_description=__doc__,
         py_modules=['flask'],
         zip_safe=False,
    
    From 75057bb4112d16acc7ed5dafce6a8168b343fd78 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Tue, 20 Apr 2010 18:44:54 +0200
    Subject: [PATCH 0119/3747] Slightly faster _tojson_filter.
    
    ---
     flask.py | 19 +++++++++----------
     1 file changed, 9 insertions(+), 10 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index e84519cd..3d136e9a 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -49,8 +49,7 @@ except (ImportError, AttributeError):
     
     # figure out if simplejson escapes slashes.  This behaviour was changed
     # from one version to another without reason.
    -if json_available:
    -    _json_escapes_slashes = '\\/' in json.dumps('/')
    +_json_escapes_slashes = json_available and '\\/' in json.dumps('/')
     
     
     class Request(RequestBase):
    @@ -273,14 +272,14 @@ def _get_package_path(name):
             return os.getcwd()
     
     
    -def _tojson_filter(string, *args, **kwargs):
    -    """Calls dumps for the template engine, escaping Slashes properly."""
    -    if __debug__:
    -        _assert_have_json()
    -    rv = json.dumps(string, *args, **kwargs)
    -    if not _json_escapes_slashes:
    -        rv = rv.replace('/', '\\/')
    -    return rv
    +if not _json_escapes_slashes:
    +    def _tojson_filter(string, *args, **kwargs):
    +        """Calls dumps for the template engine, escaping slashes properly."""
    +        if __debug__:
    +            _assert_have_json()
    +        return json.dumps(string, *args, **kwargs).replace('/', '\\/')
    +else:
    +    _tojson_filter = json.dumps
     
     
     class Flask(object):
    
    From eae44e465b4bdd9440dbfa6e0c5bc45fd2c60ed8 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Tue, 20 Apr 2010 18:48:49 +0200
    Subject: [PATCH 0120/3747] Typo in README
    
    ---
     README | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/README b/README
    index 537a71c5..73e0bbf1 100644
    --- a/README
    +++ b/README
    @@ -8,7 +8,7 @@
     
           Flask is a microframework for Python based on Werkzeug
           and Jinja2.  It's intended for small scale applications
    -      and was development with best intentions in mind.
    +      and was developped with best intentions in mind.
     
         ~ Is it ready?
     
    @@ -26,6 +26,6 @@
     
         ~ Where are the docs?
     
    -      Go to http://flask.pocoo.org/ for a prebuild version of
    +      Go to http://flask.pocoo.org/ for a prebuilt version of
           the current documentation.  Otherwise build them yourself
           from the sphinx sources in the docs folder.
    
    From af3b73f70d5fce72181530d384519ae31d77c772 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 19:03:18 +0200
    Subject: [PATCH 0121/3747] Tiny refactoring
    
    ---
     flask.py | 14 +++++---------
     1 file changed, 5 insertions(+), 9 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index 3d136e9a..85d87a67 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -47,11 +47,6 @@ except (ImportError, AttributeError):
         pkg_resources = None
     
     
    -# figure out if simplejson escapes slashes.  This behaviour was changed
    -# from one version to another without reason.
    -_json_escapes_slashes = json_available and '\\/' in json.dumps('/')
    -
    -
     class Request(RequestBase):
         """The request object used by default in flask.  Remembers the
         matched endpoint and view arguments.
    @@ -272,12 +267,13 @@ def _get_package_path(name):
             return os.getcwd()
     
     
    -if not _json_escapes_slashes:
    -    def _tojson_filter(string, *args, **kwargs):
    -        """Calls dumps for the template engine, escaping slashes properly."""
    +# figure out if simplejson escapes slashes.  This behaviour was changed
    +# from one version to another without reason.
    +if not json_available or '\\/' not in json.dumps('/'):
    +    def _tojson_filter(*args, **kwargs):
             if __debug__:
                 _assert_have_json()
    -        return json.dumps(string, *args, **kwargs).replace('/', '\\/')
    +        return json.dumps(*args, **kwargs).replace('/', '\\/')
     else:
         _tojson_filter = json.dumps
     
    
    From 3053fcdb0d6189ad1deca6fcfa16443dad4baf8c Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 20:01:00 +0200
    Subject: [PATCH 0122/3747] Make the example tests pass on Windows.
    
    Also updated the documentation regarding that.
    ---
     docs/testing.rst                    | 36 ++++++++++++++++++-----------
     examples/flaskr/flaskr_tests.py     |  9 ++++++--
     examples/minitwit/minitwit_tests.py |  9 ++++++--
     3 files changed, 37 insertions(+), 17 deletions(-)
    
    diff --git a/docs/testing.rst b/docs/testing.rst
    index 3ea1e9ba..eb4397a2 100644
    --- a/docs/testing.rst
    +++ b/docs/testing.rst
    @@ -42,25 +42,32 @@ In order to test that, we add a second module (
         class FlaskrTestCase(unittest.TestCase):
     
             def setUp(self):
    -            self.db = tempfile.NamedTemporaryFile()
    +            self.db_fd, flaskr.DATABASE = tempfile.mkstemp()
                 self.app = flaskr.app.test_client()
    -            flaskr.DATABASE = self.db.name
                 flaskr.init_db()
     
    +        def tearDown(self):
    +            os.close(self.db_fd)
    +            os.unlink(flaskr.DATABASE)
    +
         if __name__ == '__main__':
             unittest.main()
     
    -The code in the `setUp` function creates a new test client and initializes
    -a new database.  That function is called before each individual test function.
    -What the test client does is give us a simple interface to the
    -application.  We can trigger test requests to the application and the
    -client will also keep track of cookies for us.
    +The code in the :meth:`~unittest.TestCase.setUp` method creates a new test
    +client and initializes a new database.  That function is called before
    +each individual test function.  To delete the database after the test, we
    +close the file and remove it from the filesystem in the
    +:meth:`~unittest.TestCase.tearDown` method.  What the test client does is
    +give us a simple interface to the application.  We can trigger test
    +requests to the application and the client will also keep track of cookies
    +for us.
     
     Because SQLite3 is filesystem-based we can easily use the tempfile module
    -to create a temporary database and initialize it.  Just make sure that you
    -keep a reference to the :class:`~tempfile.NamedTemporaryFile` around (we
    -store it as `self.db` because of that) so that the garbage collector does
    -not remove that object and with it the database from the filesystem.
    +to create a temporary database and initialize it.  The
    +:func:`~tempfile.mkstemp` function does two things for us: it returns a
    +low-level file handle and a random file name, the latter we use as
    +database name.  We just have to keep the `db_fd` around so that we can use
    +the :func:`os.close` function to close the function.
     
     If we now run that testsuite, we should see the following output::
     
    @@ -86,11 +93,14 @@ this::
         class FlaskrTestCase(unittest.TestCase):
     
             def setUp(self):
    -            self.db = tempfile.NamedTemporaryFile()
    +            self.db_fd, flaskr.DATABASE = tempfile.mkstemp()
                 self.app = flaskr.app.test_client()
    -            flaskr.DATABASE = self.db.name
                 flaskr.init_db()
     
    +        def tearDown(self):
    +            os.close(self.db_fd)
    +            os.unlink(flaskr.DATABASE)
    +
             def test_empty_db(self):
                 rv = self.app.get('/')
                 assert 'No entries here so far' in rv.data
    diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py
    index 4355a650..9421cca6 100644
    --- a/examples/flaskr/flaskr_tests.py
    +++ b/examples/flaskr/flaskr_tests.py
    @@ -8,6 +8,7 @@
         :copyright: (c) 2010 by Armin Ronacher.
         :license: BSD, see LICENSE for more details.
     """
    +import os
     import flaskr
     import unittest
     import tempfile
    @@ -17,11 +18,15 @@ class FlaskrTestCase(unittest.TestCase):
     
         def setUp(self):
             """Before each test, set up a blank database"""
    -        self.db = tempfile.NamedTemporaryFile()
    +        self.db_fd, flaskr.DATABASE = tempfile.mkstemp()
             self.app = flaskr.app.test_client()
    -        flaskr.DATABASE = self.db.name
             flaskr.init_db()
     
    +    def tearDown(self):
    +        """Get rid of the database again after each test."""
    +        os.close(self.db_fd)
    +        os.unlink(flaskr.DATABASE)
    +
         def login(self, username, password):
             return self.app.post('/login', data=dict(
                 username=username,
    diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py
    index cd761aa6..10962142 100644
    --- a/examples/minitwit/minitwit_tests.py
    +++ b/examples/minitwit/minitwit_tests.py
    @@ -8,6 +8,7 @@
         :copyright: (c) 2010 by Armin Ronacher.
         :license: BSD, see LICENSE for more details.
     """
    +import os
     import minitwit
     import unittest
     import tempfile
    @@ -17,11 +18,15 @@ class MiniTwitTestCase(unittest.TestCase):
     
         def setUp(self):
             """Before each test, set up a blank database"""
    -        self.db = tempfile.NamedTemporaryFile()
    +        self.db_fd, minitwit.DATABASE = tempfile.mkstemp()
             self.app = minitwit.app.test_client()
    -        minitwit.DATABASE = self.db.name
             minitwit.init_db()
     
    +    def tearDown(self):
    +        """Get rid of the database again after each test."""
    +        os.close(self.db_fd)
    +        os.unlink(minitwit.DATABASE)
    +
         # helper functions
     
         def register(self, username, password, password2=None, email=None):
    
    From f6b9efc1e9b94382b877c0d1ebe87956b78d012f Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 20:03:03 +0200
    Subject: [PATCH 0123/3747] Added missing os import.
    
    ---
     docs/testing.rst | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/docs/testing.rst b/docs/testing.rst
    index eb4397a2..219825ea 100644
    --- a/docs/testing.rst
    +++ b/docs/testing.rst
    @@ -35,8 +35,9 @@ The Testing Skeleton
     In order to test that, we add a second module (
     `flaskr_tests.py`) and create a unittest skeleton there::
     
    -    import unittest
    +    import os
         import flaskr
    +    import unittest
         import tempfile
     
         class FlaskrTestCase(unittest.TestCase):
    
    From 260b4dfe61cd35373eb4993453b500e1db71a753 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 20:21:01 +0200
    Subject: [PATCH 0124/3747] Added changelog, restructured docs a bit.
    
    ---
     CHANGES            | 20 ++++++++++++++++++++
     docs/changelog.rst |  1 +
     docs/index.rst     | 18 ++++++++++++++----
     3 files changed, 35 insertions(+), 4 deletions(-)
     create mode 100644 CHANGES
     create mode 100644 docs/changelog.rst
    
    diff --git a/CHANGES b/CHANGES
    new file mode 100644
    index 00000000..0b7501cc
    --- /dev/null
    +++ b/CHANGES
    @@ -0,0 +1,20 @@
    +Flask Changelog
    +===============
    +
    +Here you can see the full list of changes between each Flask release.
    +
    +Version 0.2
    +-----------
    +
    +[unreleased; current development version]
    +
    +- various bugfixes
    +- integrated JSON support
    +- added :func:`~flask.get_template_attribute` helper function.
    +- :meth:`~flask.Flask.add_url_rule` can now also register a
    +  view function.
    +
    +Version 0.1
    +-----------
    +
    +First public preview release.
    diff --git a/docs/changelog.rst b/docs/changelog.rst
    new file mode 100644
    index 00000000..d6c5f48c
    --- /dev/null
    +++ b/docs/changelog.rst
    @@ -0,0 +1 @@
    +.. include:: ../CHANGES
    diff --git a/docs/index.rst b/docs/index.rst
    index 9b2f132b..1f3b6460 100644
    --- a/docs/index.rst
    +++ b/docs/index.rst
    @@ -25,8 +25,8 @@ following links:
     .. _Jinja2: http://jinja.pocoo.org/2/
     .. _Werkzeug: http://werkzeug.pocoo.org/
     
    -Textual Documentation
    ----------------------
    +User's Guide
    +------------
     
     This part of the documentation is written text and should give you an idea
     how to work with Flask.  It's a series of step-by-step instructions for
    @@ -43,11 +43,21 @@ web development.
        patterns/index
        deploying/index
        becomingbig
    +
    +Additional Notes
    +----------------
    +
    +Design notes, legal information and changelog are here for the interested:
    +
    +.. toctree::
    +   :maxdepth: 2
    +
        design
        license
    +   changelog
     
    -Reference
    ----------
    +API Reference
    +-------------
     
     If you are looking for information on a specific function, class or
     method, this part of the documentation is for you:
    
    From 00f87f679cdd63b642675d86d073882fb8b19430 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 21:44:52 +0200
    Subject: [PATCH 0125/3747] flask uses 127.0.0.1 as host by default now. stupid
     chrome.
    
    ---
     docs/quickstart.rst | 17 ++---------------
     flask.py            |  2 +-
     2 files changed, 3 insertions(+), 16 deletions(-)
    
    diff --git a/docs/quickstart.rst b/docs/quickstart.rst
    index 3d3e765f..e0f0749f 100644
    --- a/docs/quickstart.rst
    +++ b/docs/quickstart.rst
    @@ -30,9 +30,9 @@ because this would conflict with Flask itself.
     ::
     
         $ python hello.py
    -     * Running on http://localhost:5000/
    +     * Running on http://127.0.0.1:5000/
     
    -Head over to `http://localhost:5000/ `_, you should
    +Head over to `http://127.0.0.1:5000/ `_, you should
     see your hello world greeting.
     
     So what did that code do?
    @@ -54,19 +54,6 @@ So what did that code do?
     
     To stop the server, hit control-C.
     
    -.. admonition:: Troubleshooting
    -
    -   The browser is unable to access the server?  Sometimes this is
    -   unfortunately caused by broken IPv6 support in your operating system,
    -   browser or a combination.  For example on Snow Leopard Google Chrome is
    -   known to exhibit this behaviour.
    -
    -   If the browser does not load up the page, you can change the `app.run`
    -   call to force IPv4 usage::
    -
    -      if __name__ == '__main__':
    -          app.run(host='127.0.0.1')
    -
     
     Debug Mode
     ----------
    diff --git a/flask.py b/flask.py
    index 85d87a67..3fed49d0 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -434,7 +434,7 @@ class Flask(object):
             for func in self.template_context_processors:
                 context.update(func())
     
    -    def run(self, host='localhost', port=5000, **options):
    +    def run(self, host='127.0.0.1', port=5000, **options):
             """Runs the application on a local development server.  If the
             :attr:`debug` flag is set the server will automatically reload
             for code changes and show a debugger in case an exception happened.
    
    From 9cf2ea2abc5243d50c87612714904c6cb033f7cc Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Tue, 20 Apr 2010 21:45:36 +0200
    Subject: [PATCH 0126/3747] Documented hostname change.
    
    ---
     CHANGES | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/CHANGES b/CHANGES
    index 0b7501cc..ce7289da 100644
    --- a/CHANGES
    +++ b/CHANGES
    @@ -13,6 +13,7 @@ Version 0.2
     - added :func:`~flask.get_template_attribute` helper function.
     - :meth:`~flask.Flask.add_url_rule` can now also register a
       view function.
    +- server listens on 127.0.0.1 by default now to fix issues with chrome.
     
     Version 0.1
     -----------
    
    From 5876a8fd96efa01958904ca0796eb4018222bf25 Mon Sep 17 00:00:00 2001
    From: cgrinds 
    Date: Wed, 21 Apr 2010 02:33:32 +0800
    Subject: [PATCH 0127/3747] Merge more upstream changes
    
    ---
     docs/installation.rst | 32 ++++++++++++++------------------
     1 file changed, 14 insertions(+), 18 deletions(-)
    
    diff --git a/docs/installation.rst b/docs/installation.rst
    index f2f4905f..bb85b405 100644
    --- a/docs/installation.rst
    +++ b/docs/installation.rst
    @@ -9,10 +9,10 @@ way and why there are multiple ways.
     
     Flask depends on two external libraries: `Werkzeug
     `_ and `Jinja2 `_.
    -The first one is responsible for interfacing WSGI the latter for rendering
    +The first on is responsible for interfacing WSGI the latter to render
     templates.  Now you are maybe asking, what is WSGI?  WSGI is a standard
     in Python that is basically responsible for ensuring that your application
    -is behaving in a specific way so that you can run it on different
    +is behaving in a specific way that you can run it on different
     environments (for example on a local development server, on an Apache2, on
     lighttpd, on Google's App Engine or whatever you have in mind).
     
    @@ -26,10 +26,10 @@ Virtualenv is what you want to use during development and in production if
     you have shell access.  So first: what does virtualenv do?  If you are
     like me and you like Python, chances are you want to use it for another
     project as well.  Now the more projects you have, the more likely it is
    -that you will be working with different versions of Python itself or at
    -least an individual library.  Because let's face it: quite often libraries 
    -break backwards compatibility and it's unlikely that your application will
    -not have any dependencies, that just won't happen.  So virtualenv to the
    +that you will be working with different versions of Python itself or a
    +library involved.  Because let's face it: quite often libraries break
    +backwards compatibility and it's unlikely that your application will
    +not have any dependencies, that just won't happen.  So virtualenv for the
     rescue!
     
     It basically makes it possible to have multiple side-by-side
    @@ -47,7 +47,7 @@ or even better::
     
         $ sudo pip install virtualenv
     
    -Chances are you have virtualenv installed on your system then.  Maybe it's
    +Changes are you have virtualenv installed on your system then.  Maybe it's
     even in your package manager (on ubuntu try ``sudo apt-get install
     python-virtualenv``).
     
    @@ -69,11 +69,7 @@ within::
     Now you only have to activate it, whenever you work with it.  On OS X and
     Linux do the following::
     
    -    $ . env/bin/activate
    -
    -(Note the whitespace between the dot and the script name.  This means
    -execute this file in context of the shell.  If the dot does not work for
    -whatever reason in your shell, try substituting it with ``source``)
    +    $ source env/bin/activate
     
     If you are a Windows user, the following command is for you::
     
    @@ -101,8 +97,8 @@ This is possible as well, but I would not recommend it.  Just run
     (Run it in an Admin shell on Windows systems and without the `sudo`).
     
     
    -Living on the Edge
    -------------------
    +Leaving on the Edge
    +-------------------
     
     You want to work with the latest version of Flask, there are two ways: you
     can either let `easy_install` pull in the development version or tell it
    @@ -115,7 +111,7 @@ Get the git checkout in a new virtualenv and run in develop mode::
         Initialized empty Git repository in ~/dev/flask/.git/
         $ cd flask
         $ virtualenv env
    -    $ . env/bin/activate
    +    $ source env/bin/activate
         New python executable in env/bin/python
         Installing setuptools............done.
         $ python setup.py develop
    @@ -131,7 +127,7 @@ To just get the development version without git, do this instead::
         $ mkdir flask
         $ cd flask
         $ virtualenv env
    -    $ . env/bin/activate
    +    $ source env/bin/activate
         New python executable in env/bin/python
         Installing setuptools............done.
         $ easy_install Flask==dev
    @@ -152,7 +148,7 @@ Once you have done that it's important to add the `easy_install` command
     and other Python scripts to the path.  To do that you have to add the
     Python installation's Script folder to the `PATH` variable.
     
    -To do that, right-click on your "Computer" desktop icon and click
    +To do that, click right on your "Computer" desktop icon and click
     "Properties".  On Windows Vista and Windows 7 then click on "Advanced System
     settings", on Windows XP click on the "Advanced" tab instead.  Then click
     on the "Environment variables" button and double click on the "Path"
    @@ -165,7 +161,7 @@ the following value::
     
         ;C:\Python26\Scripts
     
    -Then you are done.  To check that it worked, open the cmd and execute
    +Then you are done.  To check if it worked, open the cmd and execute
     "easy_install".  If you have UAC enabled it should prompt you for admin
     privileges.
     
    
    From 9d340ad7477774a19a7bdffc83d6cb8cb3e05edc Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Tue, 20 Apr 2010 22:46:36 +0200
    Subject: [PATCH 0128/3747] Fix a doc oversight, and revert 5876a8fd.
    
    ---
     docs/testing.rst | 4 ++--
     flask.py         | 1 +
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/docs/testing.rst b/docs/testing.rst
    index 219825ea..0901792b 100644
    --- a/docs/testing.rst
    +++ b/docs/testing.rst
    @@ -68,7 +68,7 @@ to create a temporary database and initialize it.  The
     :func:`~tempfile.mkstemp` function does two things for us: it returns a
     low-level file handle and a random file name, the latter we use as
     database name.  We just have to keep the `db_fd` around so that we can use
    -the :func:`os.close` function to close the function.
    +the :func:`os.close` function to close the file.
     
     If we now run that testsuite, we should see the following output::
     
    @@ -76,7 +76,7 @@ If we now run that testsuite, we should see the following output::
     
         ----------------------------------------------------------------------
         Ran 0 tests in 0.000s
    -    
    +
         OK
     
     Even though it did not run any tests, we already know that our flaskr
    diff --git a/flask.py b/flask.py
    index 3fed49d0..01f24eb9 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -270,6 +270,7 @@ def _get_package_path(name):
     # figure out if simplejson escapes slashes.  This behaviour was changed
     # from one version to another without reason.
     if not json_available or '\\/' not in json.dumps('/'):
    +
         def _tojson_filter(*args, **kwargs):
             if __debug__:
                 _assert_have_json()
    
    From 7cf5a9bf6e34fc57f82e560f01c408fbe603e9d4 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Wed, 21 Apr 2010 00:40:43 +0200
    Subject: [PATCH 0129/3747] Use a tuple to store _flashes, and simplify the
     flask.Request class.
    
    ---
     flask.py | 10 +++-------
     1 file changed, 3 insertions(+), 7 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index 01f24eb9..7eca40b9 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -56,10 +56,7 @@ class Request(RequestBase):
         :attr:`~flask.Flask.request_class` to your subclass.
         """
     
    -    def __init__(self, environ):
    -        RequestBase.__init__(self, environ)
    -        self.endpoint = None
    -        self.view_args = None
    +    endpoint = view_args = None
     
         @cached_property
         def json(self):
    @@ -170,7 +167,7 @@ def flash(message):
     
         :param message: the message to be flashed.
         """
    -    session['_flashes'] = (session.get('_flashes', [])) + [message]
    +    session['_flashes'] = session.get('_flashes', ()) + (message,)
     
     
     def get_flashed_messages():
    @@ -180,8 +177,7 @@ def get_flashed_messages():
         """
         flashes = _request_ctx_stack.top.flashes
         if flashes is None:
    -        _request_ctx_stack.top.flashes = flashes = \
    -            session.pop('_flashes', [])
    +        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', ())
         return flashes
     
     
    
    From b6e73305392c9de052e51cca6fae8fee12ae231e Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Wed, 21 Apr 2010 01:23:09 +0200
    Subject: [PATCH 0130/3747] Use setdefault() because it sets session.modified
     correctly.
    
    ---
     flask.py             |  4 ++--
     tests/flask_tests.py | 12 ++++++++++++
     2 files changed, 14 insertions(+), 2 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index 7eca40b9..1f0fb7e6 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -167,7 +167,7 @@ def flash(message):
     
         :param message: the message to be flashed.
         """
    -    session['_flashes'] = session.get('_flashes', ()) + (message,)
    +    session.setdefault('_flashes', []).append(message)
     
     
     def get_flashed_messages():
    @@ -177,7 +177,7 @@ def get_flashed_messages():
         """
         flashes = _request_ctx_stack.top.flashes
         if flashes is None:
    -        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', ())
    +        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', [])
         return flashes
     
     
    diff --git a/tests/flask_tests.py b/tests/flask_tests.py
    index 6a27373b..5f07fbe8 100644
    --- a/tests/flask_tests.py
    +++ b/tests/flask_tests.py
    @@ -117,6 +117,18 @@ class BasicFunctionalityTestCase(unittest.TestCase):
                 expect_exception(flask.session.__setitem__, 'foo', 42)
                 expect_exception(flask.session.pop, 'foo')
     
    +    def test_flashes(self):
    +        app = flask.Flask(__name__)
    +        app.secret_key = 'testkey'
    +
    +        with app.test_request_context():
    +            assert not flask.session.modified
    +            flask.flash('Zap')
    +            flask.session.modified = False
    +            flask.flash('Zip')
    +            assert flask.session.modified
    +            assert list(flask.get_flashed_messages()) == ['Zap', 'Zip']
    +
         def test_request_processing(self):
             app = flask.Flask(__name__)
             evts = []
    
    From f014ce29a7cd5a3ccfabd61e7d66e017ed958e25 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Wed, 21 Apr 2010 04:46:36 +0800
    Subject: [PATCH 0131/3747] Fix a doc oversight, and revert 5876a8fd.
    
    ---
     docs/testing.rst | 4 ++--
     flask.py         | 1 +
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/docs/testing.rst b/docs/testing.rst
    index 219825ea..0901792b 100644
    --- a/docs/testing.rst
    +++ b/docs/testing.rst
    @@ -68,7 +68,7 @@ to create a temporary database and initialize it.  The
     :func:`~tempfile.mkstemp` function does two things for us: it returns a
     low-level file handle and a random file name, the latter we use as
     database name.  We just have to keep the `db_fd` around so that we can use
    -the :func:`os.close` function to close the function.
    +the :func:`os.close` function to close the file.
     
     If we now run that testsuite, we should see the following output::
     
    @@ -76,7 +76,7 @@ If we now run that testsuite, we should see the following output::
     
         ----------------------------------------------------------------------
         Ran 0 tests in 0.000s
    -    
    +
         OK
     
     Even though it did not run any tests, we already know that our flaskr
    diff --git a/flask.py b/flask.py
    index 3fed49d0..01f24eb9 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -270,6 +270,7 @@ def _get_package_path(name):
     # figure out if simplejson escapes slashes.  This behaviour was changed
     # from one version to another without reason.
     if not json_available or '\\/' not in json.dumps('/'):
    +
         def _tojson_filter(*args, **kwargs):
             if __debug__:
                 _assert_have_json()
    
    From 3eacc1d06816b2544e0242665fe909b24b027b21 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Wed, 21 Apr 2010 06:40:43 +0800
    Subject: [PATCH 0132/3747] Use a tuple to store _flashes, and simplify the
     flask.Request class.
    
    ---
     flask.py | 10 +++-------
     1 file changed, 3 insertions(+), 7 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index 01f24eb9..7eca40b9 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -56,10 +56,7 @@ class Request(RequestBase):
         :attr:`~flask.Flask.request_class` to your subclass.
         """
     
    -    def __init__(self, environ):
    -        RequestBase.__init__(self, environ)
    -        self.endpoint = None
    -        self.view_args = None
    +    endpoint = view_args = None
     
         @cached_property
         def json(self):
    @@ -170,7 +167,7 @@ def flash(message):
     
         :param message: the message to be flashed.
         """
    -    session['_flashes'] = (session.get('_flashes', [])) + [message]
    +    session['_flashes'] = session.get('_flashes', ()) + (message,)
     
     
     def get_flashed_messages():
    @@ -180,8 +177,7 @@ def get_flashed_messages():
         """
         flashes = _request_ctx_stack.top.flashes
         if flashes is None:
    -        _request_ctx_stack.top.flashes = flashes = \
    -            session.pop('_flashes', [])
    +        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', ())
         return flashes
     
     
    
    From dbe0df756bf587b242f2406bef9e7e8ef26f0813 Mon Sep 17 00:00:00 2001
    From: florentx 
    Date: Wed, 21 Apr 2010 07:23:09 +0800
    Subject: [PATCH 0133/3747] Use setdefault() because it sets session.modified
     correctly.
    
    ---
     flask.py             |  4 ++--
     tests/flask_tests.py | 12 ++++++++++++
     2 files changed, 14 insertions(+), 2 deletions(-)
    
    diff --git a/flask.py b/flask.py
    index 7eca40b9..1f0fb7e6 100644
    --- a/flask.py
    +++ b/flask.py
    @@ -167,7 +167,7 @@ def flash(message):
     
         :param message: the message to be flashed.
         """
    -    session['_flashes'] = session.get('_flashes', ()) + (message,)
    +    session.setdefault('_flashes', []).append(message)
     
     
     def get_flashed_messages():
    @@ -177,7 +177,7 @@ def get_flashed_messages():
         """
         flashes = _request_ctx_stack.top.flashes
         if flashes is None:
    -        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', ())
    +        _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', [])
         return flashes
     
     
    diff --git a/tests/flask_tests.py b/tests/flask_tests.py
    index 6a27373b..5f07fbe8 100644
    --- a/tests/flask_tests.py
    +++ b/tests/flask_tests.py
    @@ -117,6 +117,18 @@ class BasicFunctionalityTestCase(unittest.TestCase):
                 expect_exception(flask.session.__setitem__, 'foo', 42)
                 expect_exception(flask.session.pop, 'foo')
     
    +    def test_flashes(self):
    +        app = flask.Flask(__name__)
    +        app.secret_key = 'testkey'
    +
    +        with app.test_request_context():
    +            assert not flask.session.modified
    +            flask.flash('Zap')
    +            flask.session.modified = False
    +            flask.flash('Zip')
    +            assert flask.session.modified
    +            assert list(flask.get_flashed_messages()) == ['Zap', 'Zip']
    +
         def test_request_processing(self):
             app = flask.Flask(__name__)
             evts = []
    
    From 7e8019565f79e157fcc13cf285247070d69ef889 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Wed, 21 Apr 2010 10:37:05 +0200
    Subject: [PATCH 0134/3747] Added florentx to the AUTHORS file
    
    ---
     AUTHORS | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/AUTHORS b/AUTHORS
    index 92379c48..2c125e11 100644
    --- a/AUTHORS
    +++ b/AUTHORS
    @@ -11,3 +11,4 @@ Patches and Suggestions
     
     - Chris Edgemon
     - Chris Grindstaff
    +- Florent Xicluna
    
    From 3261357ba7f59e2eabeb96127b6253d1466b4db0 Mon Sep 17 00:00:00 2001
    From: Armin Ronacher 
    Date: Wed, 21 Apr 2010 18:39:25 +0200
    Subject: [PATCH 0135/3747] Removed website from main branch.  Makes no sense
     to be there
    
    ---
     Makefile           |   3 --
     website/404.html   |  47 --------------------------
     website/index.html |  81 ---------------------------------------------
     website/logo.png   | Bin 23478 -> 0 bytes
     website/mask.png   | Bin 1889 -> 0 bytes
     website/ship.png   | Bin 62587 -> 0 bytes
     6 files changed, 131 deletions(-)
     delete mode 100644 website/404.html
     delete mode 100644 website/index.html
     delete mode 100644 website/logo.png
     delete mode 100644 website/mask.png
     delete mode 100644 website/ship.png
    
    diff --git a/Makefile b/Makefile
    index 0be5e6bc..c62cf587 100644
    --- a/Makefile
    +++ b/Makefile
    @@ -10,8 +10,5 @@ clean-pyc:
     	find . -name '*.pyo' -exec rm -f {} +
     	find . -name '*~' -exec rm -f {} +
     
    -upload-website:
    -	scp -r website/* pocoo.org:/var/www/flask.pocoo.org/
    -
     upload-docs:
     	$(MAKE) -C docs dirhtml && scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/
    diff --git a/website/404.html b/website/404.html
    deleted file mode 100644
    index c4de489d..00000000
    --- a/website/404.html
    +++ /dev/null
    @@ -1,47 +0,0 @@
    -
    -Chapter 404: The Lost Page
    -
    -

    Chapter 404: The Lost Page

    -

    A careful and diligent search has been made for the desired page, but it just cannot be found. -

    And so they returned to familiar waters. diff --git a/website/index.html b/website/index.html deleted file mode 100644 index 5db6b850..00000000 --- a/website/index.html +++ /dev/null @@ -1,81 +0,0 @@ - -Flask (A Python Microframework) - - -

    -

    Flask

    -

    because sometimes a pocket knife is not enough -

    - Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions. - And before you ask: It's BSD licensed! -
    -

    Flask is Fun

    -
    from flask import Flask
    -app = Flask(__name__)
    -
    -@app.route("/")
    -def hello():
    -    return "Hello World!"
    -
    -if __name__ == "__main__":
    -    app.run()
    -

    And Easy to Setup

    -
    $ easy_install Flask
    -$ python hello.py
    - * Running on http://localhost:5000/
    -

    Interested?

    - -

    What’s in the Box?

    - -

    What do Flask Apps look like?

    -

    - If you are looking for some example code of applications written with Flask, - have a look at the sources of the examples on github: -

    -

    Contribute

    -

    Found a bug? Have a good idea for improving Flask? Head over to - Flask's github page and - create a new ticket or fork. If you just want to chat with fellow - developers, go to #pocoo on irc.freenode.net. - -

    - -Fork me on GitHub diff --git a/website/logo.png b/website/logo.png deleted file mode 100644 index f255eece8157a0ce6baf7e2e0b297c31ce153405..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23478 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

    F+w9=+U;Y2tlJk)x%NnDhpT;iDidr%j zgWW4Dz`raPaHqcv(AN@U=Ap|?L)*M>0K4$F!{`Qh z_&`S5CdNq$hbeloP&SAQjwcI6xds9b4g~>)8mry-#|^PLj}a2^?sCW=$jzKgVGY)! z$e~4_eD~GG;uj|A0pD-E0LqR)zwd@WI zcQNkZeMk`f7Q-^b@=L5m<}~J?gaelL3&AZ~{E(gwJHyW2#n( z3VRd41K9Hft1UU0+n0Y`4%EgO4CgUYf&e*fl3!#%I8OUh5C8x=0gfnc-}1&h3L3wTHVw Tn_3s>fRC=WzE<%iyZiqKKyJ}l From 1cb75df8f1f9e7a768254fe2c9e2997f21152665 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Apr 2010 18:39:51 +0200 Subject: [PATCH 0136/3747] Add leftovers from website branch to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5250e072..148e5029 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ env dist *.egg-info +_mailinglist From ebb07a06567d72acf47b3ee5553053216f978305 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Apr 2010 21:10:57 +0200 Subject: [PATCH 0137/3747] Better English. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e0f0749f..526d8f7b 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -285,7 +285,7 @@ the application secure. Because of that Flask configures the `Jinja2 To render a template you can use the :func:`~flask.render_template` method. All you have to do is to provide the name of the template and the variables you want to pass to the template engine as keyword arguments. -Here a simple example of how to render a template:: +Here's a simple example of how to render a template:: from flask import render_template From be15340e0f65e8785aff3033f0b2732ae31c6b81 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Apr 2010 21:47:45 +0200 Subject: [PATCH 0138/3747] Added mailinglist link to the README --- README | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README b/README index 73e0bbf1..e9eb1c7b 100644 --- a/README +++ b/README @@ -29,3 +29,8 @@ Go to http://flask.pocoo.org/ for a prebuilt version of the current documentation. Otherwise build them yourself from the sphinx sources in the docs folder. + + ~ Where can I get help? + + Either use the #pocoo IRC channel on irc.freenode.net or + ask on the mailinglist: http://flask.pocoo.org/mailinglist/ From dc3f13df54ba91649ecec2bdee6c24ca31a400a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 12:17:12 +0200 Subject: [PATCH 0139/3747] Corrected validator usage in the wtforms docs. --- docs/patterns/wtforms.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 4a836975..bbceee8a 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -22,9 +22,11 @@ This is an example form for a typical registration page:: class RegistrationForm(Form): username = TextField('Username', [validators.Length(min=4, max=25)]) email = TextField('Email Address', [validators.Length(min=6, max=35)]) - password = PasswordField('New Password', [validators.Required()]) - confirm = PasswordField('Repeat Password', [validators.EqualTo( - 'confirm', message='Passwords must match')]) + password = PasswordField('New Password', [ + validators.Required(), + validators.EqualTo('confirm', message='Passwords must match') + ]) + confirm = PasswordField('Repeat Password') accept_tos = BooleanField('I accept the TOS', [validators.Required()]) In the View From 88f671aaaef334caf3231095b119cac094ff5ffb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 16:04:54 +0200 Subject: [PATCH 0140/3747] Another try for #12 --- flask.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/flask.py b/flask.py index 1f0fb7e6..4c7a1cc4 100644 --- a/flask.py +++ b/flask.py @@ -275,6 +275,41 @@ else: _tojson_filter = json.dumps +class _ModuleSetupState(object): + + def __init__(self, app, url_prefix=None): + self.app = app + self.url_prefix = url_prefix + + +class Module(object): + """Container object that enables pluggable applications""" + + def __init__(self, name, url_prefix=None, package_name=None): + self.name = name + self.package_name = package_name + self.url_prefix = url_prefix + self._register_events = [] + + def route(self, rule, **options): + def decorator(f): + self.add_url_rule(rule, f.__name__, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint, view_func=None, **options): + self._record(self._register_rule, (rule, endpoint, view_func, options)) + + def _record(self, func, args): + self._register_events.append((func, args)) + + def _register_rule(self, state, rule, endpoint, view_func, options): + if self.url_prefix: + rule = state.url_prefix + rule + self.app.add_url_rule(rule, '%s.%s' % (self.name, endpoint), + view_func, **options) + + class Flask(object): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -506,6 +541,13 @@ class Flask(object): """ session.save_cookie(response, self.session_cookie_name) + def register_module(self, module, **options): + """Registers a module with this application.""" + options.setdefault('url_prefix', self.url_prefix) + state = _ModuleSetupState(app, options) + for func, args in module._register_events: + func(state, *args) + def add_url_rule(self, rule, endpoint, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the From a862ead5f22f9d4592c1c85bda285a4ded59b631 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 16:30:04 +0200 Subject: [PATCH 0141/3747] Warn on None responses. --- flask.py | 7 ++++++- tests/flask_tests.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 1f0fb7e6..10feaf22 100644 --- a/flask.py +++ b/flask.py @@ -687,7 +687,7 @@ class Flask(object): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. - The following types are allowd for `rv`: + The following types are allowed for `rv`: ======================= =========================================== :attr:`response_class` the object is returned unchanged @@ -703,6 +703,11 @@ class Flask(object): :param rv: the return value from the view function """ + if rv is None: + from warnings import warn + warn(Warning('View function did not return a response'), + stacklevel=2) + return u'' if isinstance(rv, self.response_class): return rv if isinstance(rv, basestring): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5f07fbe8..917f4168 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -15,6 +15,7 @@ import sys import flask import unittest import tempfile +import warnings example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -224,6 +225,19 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert flask.url_for('static', filename='index.html') \ == '/static/index.html' + def test_none_response(self): + warnings.filterwarnings('error', 'View function did not return') + app = flask.Flask(__name__) + @app.route('/') + def test(): + return None + try: + app.test_client().get('/') + except Warning: + pass + else: + assert "Expected warning" + class JSONTestCase(unittest.TestCase): From 444e6425312a16589130ff36f2830b6f97fbbd4f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 16:44:49 +0200 Subject: [PATCH 0142/3747] extended URL generation --- flask.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/flask.py b/flask.py index 4c7a1cc4..e569aaaa 100644 --- a/flask.py +++ b/flask.py @@ -56,7 +56,7 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - endpoint = view_args = None + endpoint = view_args = routing_exception = None @cached_property def json(self): @@ -117,6 +117,12 @@ class _RequestContext(object): self.g = _RequestGlobals() self.flashes = None + try: + self.request.endpoint, self.request.view_args = \ + self.url_adapter.match() + except HTTPException, e: + self.request.routing_exception = e + def __enter__(self): _request_ctx_stack.push(self) @@ -134,7 +140,12 @@ def url_for(endpoint, **values): :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule """ - return _request_ctx_stack.top.url_adapter.build(endpoint, values) + ctx = _request_ctx_stack.top + if '.' not in endpoint and \ + ctx.request.endpoint is not None \ + and '.' in ctx.request.endpoint: + endpoint = ctx.request.endpoint.rsplit('.', 1)[0] + '.' + endpoint + return ctx.url_adapter.build(endpoint.lstrip('.'), values) def get_template_attribute(template_name, attribute): @@ -548,7 +559,7 @@ class Flask(object): for func, args in module._register_events: func(state, *args) - def add_url_rule(self, rule, endpoint, view_func=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. @@ -571,7 +582,7 @@ class Flask(object): app.view_functions['index'] = index .. versionchanged:: 0.2 - `view_func` parameter added + `view_func` parameter added. :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask @@ -582,6 +593,10 @@ class Flask(object): :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object """ + if endpoint is None: + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + endpoint = view_func.__name__ options['endpoint'] = endpoint options.setdefault('methods', ('GET',)) self.url_map.add(Rule(rule, **options)) @@ -654,7 +669,7 @@ class Flask(object): :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) + self.add_url_rule(rule, None, f, **options) return f return decorator @@ -696,24 +711,17 @@ class Flask(object): self.template_context_processors.append(f) return f - def match_request(self): - """Matches the current request against the URL map and also - stores the endpoint and view arguments on the request object - is successful, otherwise the exception is stored. - """ - rv = _request_ctx_stack.top.url_adapter.match() - request.endpoint, request.view_args = rv - return rv - def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. """ + req = _request_ctx_stack.top.request try: - endpoint, values = self.match_request() - return self.view_functions[endpoint](**values) + if req.routing_exception is not None: + raise req.routing_exception + return self.view_functions[req.endpoint](**req.view_args) except HTTPException, e: handler = self.error_handlers.get(e.code) if handler is None: From e0148a00c0f468784052926f80e8112be7686470 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 17:01:20 +0200 Subject: [PATCH 0143/3747] Basic module support is working, but does not look very nice. --- flask.py | 55 +++++++++++++++++++++++++++++++++----------- tests/flask_tests.py | 25 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/flask.py b/flask.py index e569aaaa..94072ce9 100644 --- a/flask.py +++ b/flask.py @@ -13,6 +13,7 @@ from __future__ import with_statement import os import sys +from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ @@ -58,6 +59,12 @@ class Request(RequestBase): endpoint = view_args = routing_exception = None + @property + def module(self): + """The name of the current module""" + if self.endpoint and '.' in self.endpoint: + return self.endpoint.rsplit('.', 1)[0] + @cached_property def json(self): """If the mimetype is `application/json` this will contain the @@ -141,11 +148,13 @@ def url_for(endpoint, **values): :param values: the variable arguments of the URL rule """ ctx = _request_ctx_stack.top - if '.' not in endpoint and \ - ctx.request.endpoint is not None \ - and '.' in ctx.request.endpoint: - endpoint = ctx.request.endpoint.rsplit('.', 1)[0] + '.' + endpoint - return ctx.url_adapter.build(endpoint.lstrip('.'), values) + if '.' not in endpoint: + mod = ctx.request.module + if mod is not None: + endpoint = mod + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] + return ctx.url_adapter.build(endpoint, values) def get_template_attribute(template_name, attribute): @@ -311,6 +320,16 @@ class Module(object): def add_url_rule(self, rule, endpoint, view_func=None, **options): self._record(self._register_rule, (rule, endpoint, view_func, options)) + def before_request(self, f): + self._record(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f), ()) + return f + + def after_request(self, f): + self._record(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f), ()) + return f + def _record(self, func, args): self._register_events.append((func, args)) @@ -402,14 +421,14 @@ class Flask(object): #: getting hold of the currently logged in user. #: To register a function here, use the :meth:`before_request` #: decorator. - self.before_request_funcs = [] + self.before_request_funcs = {} #: a list of functions that are called at the end of the #: request. The function is passed the current response #: object and modify it in place or replace it. #: To register a function here use the :meth:`after_request` #: decorator. - self.after_request_funcs = [] + self.after_request_funcs = {} #: a list of functions that are called without arguments #: to populate the template context. Each returns a dictionary @@ -698,12 +717,12 @@ class Flask(object): def before_request(self, f): """Registers a function to run before each request.""" - self.before_request_funcs.append(f) + self.before_request_funcs.setdefault(None, []).append(f) return f def after_request(self, f): """Register a function to be run after each request.""" - self.after_request_funcs.append(f) + self.after_request_funcs.setdefault(None, []).append(f) return f def context_processor(self, f): @@ -768,7 +787,11 @@ class Flask(object): if it was the return value from the view and further request handling is stopped. """ - for func in self.before_request_funcs: + funcs = self.before_request_funcs.get(None, ()) + mod = request.module + if mod and mod in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[mod]) + for func in funcs: rv = func() if rv is not None: return rv @@ -782,10 +805,14 @@ class Flask(object): :return: a new response object or the same, has to be an instance of :attr:`response_class`. """ - session = _request_ctx_stack.top.session - if not isinstance(session, _NullSession): - self.save_session(session, response) - for handler in self.after_request_funcs: + ctx = _request_ctx_stack.top + mod = ctx.request.module + if not isinstance(ctx.session, _NullSession): + self.save_session(ctx.session, response) + funcs = self.after_request_funcs.get(None, ()) + if mod and mod in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[mod]) + for handler in funcs: response = handler(response) return response diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5f07fbe8..2f372514 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -298,6 +298,31 @@ class TemplatingTestCase(unittest.TestCase): assert macro('World') == 'Hello World!' +class ModuleTestCase(unittest.TestCase): + + def test_basic_module(self): + app = flask.Flask(__name__) + admin = flask.Module('admin', url_prefix='/admin') + @admin.route('/') + def index(): + return 'admin index' + @admin.route('/login') + def login(): + return 'admin login' + @admin.route('/logout') + def logout(): + return 'admin logout' + @app.route('/') + def index(): + return 'the index' + app.register_module('admin', admin) + c = app.test_client() + assert c.get('/').data == 'the index' + assert c.get('/admin/').data == 'admin index' + assert c.get('/admin/login').data == 'admin login' + assert c.get('/admin/logout').data == 'admin logout' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase From 5c52fe980eda6065edcc39e6dd346636b1c40768 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 17:10:22 +0200 Subject: [PATCH 0144/3747] Added request/response processing based on modules. --- flask.py | 12 +++++++----- tests/flask_tests.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/flask.py b/flask.py index 94072ce9..8d516181 100644 --- a/flask.py +++ b/flask.py @@ -336,8 +336,8 @@ class Module(object): def _register_rule(self, state, rule, endpoint, view_func, options): if self.url_prefix: rule = state.url_prefix + rule - self.app.add_url_rule(rule, '%s.%s' % (self.name, endpoint), - view_func, **options) + state.app.add_url_rule(rule, '%s.%s' % (self.name, endpoint), + view_func, **options) class Flask(object): @@ -573,8 +573,8 @@ class Flask(object): def register_module(self, module, **options): """Registers a module with this application.""" - options.setdefault('url_prefix', self.url_prefix) - state = _ModuleSetupState(app, options) + options.setdefault('url_prefix', module.url_prefix) + state = _ModuleSetupState(self, **options) for func, args in module._register_events: func(state, *args) @@ -809,9 +809,11 @@ class Flask(object): mod = ctx.request.module if not isinstance(ctx.session, _NullSession): self.save_session(ctx.session, response) - funcs = self.after_request_funcs.get(None, ()) + funcs = () if mod and mod in self.after_request_funcs: funcs = chain(funcs, self.after_request_funcs[mod]) + if None in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[None]) for handler in funcs: response = handler(response) return response diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2f372514..e57b2f32 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -315,13 +315,48 @@ class ModuleTestCase(unittest.TestCase): @app.route('/') def index(): return 'the index' - app.register_module('admin', admin) + app.register_module(admin) c = app.test_client() assert c.get('/').data == 'the index' assert c.get('/admin/').data == 'admin index' assert c.get('/admin/login').data == 'admin login' assert c.get('/admin/logout').data == 'admin logout' + def test_request_processing(self): + catched = [] + app = flask.Flask(__name__) + admin = flask.Module('admin', url_prefix='/admin') + @admin.before_request + def before_admin_request(): + catched.append('before-admin') + @admin.after_request + def after_admin_request(response): + catched.append('after-admin') + return response + @admin.route('/') + def index(): + return 'the admin' + @app.before_request + def before_request(): + catched.append('before-app') + @app.after_request + def after_request(response): + catched.append('after-app') + return response + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + + assert c.get('/').data == 'the index' + assert catched == ['before-app', 'after-app'] + del catched[:] + + assert c.get('/admin/').data == 'the admin' + assert catched == ['before-app', 'before-admin', + 'after-admin', 'after-app'] + def suite(): from minitwit_tests import MiniTwitTestCase @@ -334,6 +369,7 @@ def suite(): suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) suite.addTest(unittest.makeSuite(FlaskrTestCase)) + suite.addTest(unittest.makeSuite(ModuleTestCase)) return suite From 2f181e19496a1fe693d9b20260fe961bb406c53b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Apr 2010 17:16:59 +0200 Subject: [PATCH 0145/3747] Removed unused package_name attribute --- flask.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flask.py b/flask.py index 8d516181..c1a03f26 100644 --- a/flask.py +++ b/flask.py @@ -305,9 +305,8 @@ class _ModuleSetupState(object): class Module(object): """Container object that enables pluggable applications""" - def __init__(self, name, url_prefix=None, package_name=None): + def __init__(self, name, url_prefix=None): self.name = name - self.package_name = package_name self.url_prefix = url_prefix self._register_events = [] From 67f4b0f31538832f06b9351d15e30c85f449ac7e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Apr 2010 17:07:16 +0200 Subject: [PATCH 0146/3747] Modules are now instanciated with the package name. This makes it possible to load resources from the folder the module is located in. --- flask.py | 165 ++++++++++++++++++++++++------------------- tests/flask_tests.py | 4 +- 2 files changed, 96 insertions(+), 73 deletions(-) diff --git a/flask.py b/flask.py index c1a03f26..e07b7923 100644 --- a/flask.py +++ b/flask.py @@ -295,6 +295,43 @@ else: _tojson_filter = json.dumps +class _PackageBoundObject(object): + + def __init__(self, import_name): + #: the name of the package or module. Do not change this once + #: it was set by the constructor. + self.import_name = import_name + + #: where is the app root located? + self.root_path = _get_package_path(self.import_name) + + def open_resource(self, resource): + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schemal.sql + /static + /style.css + /template + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + if pkg_resources is None: + return open(os.path.join(self.root_path, resource), 'rb') + return pkg_resources.resource_stream(self.import_name, resource) + + class _ModuleSetupState(object): def __init__(self, app, url_prefix=None): @@ -302,44 +339,53 @@ class _ModuleSetupState(object): self.url_prefix = url_prefix -class Module(object): +class Module(_PackageBoundObject): """Container object that enables pluggable applications""" - def __init__(self, name, url_prefix=None): + def __init__(self, import_name, name=None, url_prefix=None): + if name is None: + assert '.' in import_name, 'name required if package name ' \ + 'does not point to a submodule' + name = import_name.rsplit('.', 1)[1] + _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix self._register_events = [] def route(self, rule, **options): + """Like :meth:`flask.Flask.route` but for a module""" def decorator(f): self.add_url_rule(rule, f.__name__, f, **options) return f return decorator def add_url_rule(self, rule, endpoint, view_func=None, **options): - self._record(self._register_rule, (rule, endpoint, view_func, options)) + """Like :meth:`flask.Flask.add_url_rule` but for a module""" + def register_rule(state): + the_rule = rule + if self.url_prefix: + the_rule = state.url_prefix + rule + state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), + view_func, **options) + self._record(register_rule) def before_request(self, f): + """Like :meth:`flask.Flask.before_request` but for a module""" self._record(lambda s: s.app.before_request_funcs - .setdefault(self.name, []).append(f), ()) + .setdefault(self.name, []).append(f)) return f def after_request(self, f): + """Like :meth:`flask.Flask.after_request` but for a module""" self._record(lambda s: s.app.after_request_funcs - .setdefault(self.name, []).append(f), ()) + .setdefault(self.name, []).append(f)) return f - def _record(self, func, args): - self._register_events.append((func, args)) - - def _register_rule(self, state, rule, endpoint, view_func, options): - if self.url_prefix: - rule = state.url_prefix + rule - state.app.add_url_rule(rule, '%s.%s' % (self.name, endpoint), - view_func, **options) + def _record(self, func): + self._register_events.append(func) -class Flask(object): +class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for @@ -386,7 +432,9 @@ class Flask(object): extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) - def __init__(self, package_name): + def __init__(self, import_name): + _PackageBoundObject.__init__(self, import_name) + #: the debug flag. Set this to `True` to enable debugging of #: the application. In debug mode the debugger will kick in #: when an unhandled exception ocurrs and the integrated server @@ -394,13 +442,6 @@ class Flask(object): #: code are detected. self.debug = False - #: the name of the package or module. Do not change this once - #: it was set by the constructor. - self.package_name = package_name - - #: where is the app root located? - self.root_path = _get_package_path(self.package_name) - #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. @@ -414,27 +455,31 @@ class Flask(object): #: decorator. self.error_handlers = {} - #: a list of functions that should be called at the beginning - #: of the request before request dispatching kicks in. This - #: can for example be used to open database connections or - #: getting hold of the currently logged in user. - #: To register a function here, use the :meth:`before_request` - #: decorator. + #: a dictionary with lists of functions that should be called at the + #: beginning of the request. The key of the dictionary is the name of + #: the module this function is active for, `None` for all requests. + #: This can for example be used to open database connections or + #: getting hold of the currently logged in user. To register a + #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} - #: a list of functions that are called at the end of the - #: request. The function is passed the current response - #: object and modify it in place or replace it. - #: To register a function here use the :meth:`after_request` - #: decorator. + #: a dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the module + #: this function is active for, `None` for all requests. This can for + #: example be used to open database connections or getting hold of the + #: currently logged in user. To register a function here, use the + #: :meth:`before_request` decorator. self.after_request_funcs = {} - #: a list of functions that are called without arguments - #: to populate the template context. Each returns a dictionary - #: that the template context is updated with. - #: To register a function here, use the :meth:`context_processor` - #: decorator. - self.template_context_processors = [_default_template_ctx_processor] + #: a dictionary with list of functions that are called without arguments + #: to populate the template context. They key of the dictionary is the + #: name of the module this function is active for, `None` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = { + None: [_default_template_ctx_processor] + } #: the :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created @@ -457,7 +502,7 @@ class Flask(object): self.add_url_rule(self.static_path + '/', build_only=True, endpoint='static') if pkg_resources is not None: - target = (self.package_name, 'static') + target = (self.import_name, 'static') else: target = os.path.join(self.root_path, 'static') self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { @@ -483,7 +528,7 @@ class Flask(object): """ if pkg_resources is None: return FileSystemLoader(os.path.join(self.root_path, 'templates')) - return PackageLoader(self.package_name) + return PackageLoader(self.import_name) def update_template_context(self, context): """Update the template context with some commonly used variables. @@ -492,7 +537,11 @@ class Flask(object): :param context: the context as a dictionary that is updated in place to add extra variables. """ - for func in self.template_context_processors: + funcs = self.template_context_processors[None] + mod = _request_ctx_stack.top.request.module + if mod is not None and mod in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[mod]) + for func in funcs: context.update(func()) def run(self, host='127.0.0.1', port=5000, **options): @@ -521,32 +570,6 @@ class Flask(object): from werkzeug import Client return Client(self, self.response_class, use_cookies=True) - def open_resource(self, resource): - """Opens a resource from the application's resource folder. To see - how this works, consider the following folder structure:: - - /myapplication.py - /schemal.sql - /static - /style.css - /template - /layout.html - /index.html - - If you want to open the `schema.sql` file you would do the - following:: - - with app.open_resource('schema.sql') as f: - contents = f.read() - do_something_with(contents) - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - """ - if pkg_resources is None: - return open(os.path.join(self.root_path, resource), 'rb') - return pkg_resources.resource_stream(self.package_name, resource) - def open_session(self, request): """Creates or opens a new session. Default implementation stores all session data in a signed cookie. This requires that the @@ -574,8 +597,8 @@ class Flask(object): """Registers a module with this application.""" options.setdefault('url_prefix', module.url_prefix) state = _ModuleSetupState(self, **options) - for func, args in module._register_events: - func(state, *args) + for func in module._register_events: + func(state) def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` @@ -726,7 +749,7 @@ class Flask(object): def context_processor(self, f): """Registers a template context processor function.""" - self.template_context_processors.append(f) + self.template_context_processors[None].append(f) return f def dispatch_request(self): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e57b2f32..15b53651 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -302,7 +302,7 @@ class ModuleTestCase(unittest.TestCase): def test_basic_module(self): app = flask.Flask(__name__) - admin = flask.Module('admin', url_prefix='/admin') + admin = flask.Module(__name__, 'admin', url_prefix='/admin') @admin.route('/') def index(): return 'admin index' @@ -325,7 +325,7 @@ class ModuleTestCase(unittest.TestCase): def test_request_processing(self): catched = [] app = flask.Flask(__name__) - admin = flask.Module('admin', url_prefix='/admin') + admin = flask.Module(__name__, 'admin', url_prefix='/admin') @admin.before_request def before_admin_request(): catched.append('before-admin') From c0a9f5d0c4f09dbe8b744609552cbbfa9f14fbc0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Apr 2010 17:35:25 +0200 Subject: [PATCH 0147/3747] Documentation update for the flask module support. --- docs/api.rst | 9 ++++++ flask.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d285dbfd..0d8b6cf5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,6 +15,15 @@ Application Object .. autoclass:: Flask :members: + :inherited-members: + + +Module Objects +-------------- + +.. autoclass:: Module + :members: + :inherited-members: Incoming Request Data --------------------- diff --git a/flask.py b/flask.py index e07b7923..6d242aa1 100644 --- a/flask.py +++ b/flask.py @@ -340,7 +340,49 @@ class _ModuleSetupState(object): class Module(_PackageBoundObject): - """Container object that enables pluggable applications""" + """Container object that enables pluggable applications. A module can + be used to organize larger applications. They represent blueprints that, + in combination with a :class:`Flask` object are used to create a large + application. + + A module is like an application bound to an `import_name`. Multiple + modules can share the same import names, but in that case a `name` has + to be provided to keep them apart. If different import names are used, + the rightmost part of the import name is used as name. + + Here an example structure for a larger appliation:: + + /myapplication + /__init__.py + /views + /__init__.py + /admin.py + /frontend.py + + The `myapplication/__init__.py` can look like this:: + + from flask import Flask + from myapplication.views.admin import admin + from myapplication.views.frontend import frontend + + app = Flask(__name__) + app.register_module(admin, url_prefix='/admin') + app.register_module(frontend) + + And here an example view module (`myapplication/views/admin.py`):: + + from flask import Module + + admin = Module(__name__) + + @admin.route('/') + def index(): + pass + + @admin.route('/login') + def login(): + pass + """ def __init__(self, import_name, name=None, url_prefix=None): if name is None: @@ -353,14 +395,18 @@ class Module(_PackageBoundObject): self._register_events = [] def route(self, rule, **options): - """Like :meth:`flask.Flask.route` but for a module""" + """Like :meth:`Flask.route` but for a module. The endpoint for the + :func:`url_for` function is prefixed with the name of the module. + """ def decorator(f): self.add_url_rule(rule, f.__name__, f, **options) return f return decorator def add_url_rule(self, rule, endpoint, view_func=None, **options): - """Like :meth:`flask.Flask.add_url_rule` but for a module""" + """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for + the :func:`url_for` function is prefixed with the name of the module. + """ def register_rule(state): the_rule = rule if self.url_prefix: @@ -370,17 +416,39 @@ class Module(_PackageBoundObject): self._record(register_rule) def before_request(self, f): - """Like :meth:`flask.Flask.before_request` but for a module""" + """Like :meth:`Flask.before_request` but for a module. This function + is only executed before each request that is handled by a function of + that module. + """ self._record(lambda s: s.app.before_request_funcs .setdefault(self.name, []).append(f)) return f + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + def after_request(self, f): - """Like :meth:`flask.Flask.after_request` but for a module""" + """Like :meth:`Flask.after_request` but for a module. This function + is only executed after each request that is handled by a function of + that module. + """ self._record(lambda s: s.app.after_request_funcs .setdefault(self.name, []).append(f)) return f + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a module. Such a function + is executed after each request. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + def _record(self, func): self._register_events.append(func) @@ -594,7 +662,11 @@ class Flask(_PackageBoundObject): session.save_cookie(response, self.session_cookie_name) def register_module(self, module, **options): - """Registers a module with this application.""" + """Registers a module with this application. The keyword argument + of this function are the same as the ones for the constructor of the + :class:`Module` class and will override the values of the module if + provided. + """ options.setdefault('url_prefix', module.url_prefix) state = _ModuleSetupState(self, **options) for func in module._register_events: From ef34638f5d24c07a5582cbb60755fd4dfbec9a79 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Apr 2010 17:46:06 +0200 Subject: [PATCH 0148/3747] Updated docs for external server information. This fixes #14. --- docs/quickstart.rst | 18 ++++++++++++++++++ docs/tutorial/setup.rst | 13 ++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 526d8f7b..6d641d26 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -54,6 +54,24 @@ So what did that code do? To stop the server, hit control-C. +.. _public-server: + +.. admonition:: Externally Visible Server + + If you run the server you will notice that the server is only available + from your own computer, not from any other in the network. This is the + default because in debugging mode a user of the application can execute + arbitrary Python code on your computer. If you have `debug` disabled + or trust the users on your network, you can make the server publicly + available. + + Just change the call of the :meth:`~flask.Flask.run` method to look + like this:: + + app.run(host='0.0.0.0') + + This tells your operating system to listen on a public IP. + Debug Mode ---------- diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 24b76561..1214c22f 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -57,13 +57,8 @@ without problems. When you head over to the server you will get an 404 page not found error because we don't have any views yet. But we will focus on that a little later. First we should get the database working. -.. admonition:: Troubleshooting +.. admonition:: Externally Visible Server - If you notice later that the browser cannot connect to the server - during development, you might want to try this line instead:: - - app.run(host='127.0.0.1') - - In a nutshell: Werkzeug starts up as IPv6 on many operating systems by - default and not every browser is happy with that. This forces IPv4 - usage. + Want your server to be publically available? Check out the + :ref:`externally visible server ` section for more + information. From 0da39c76e3c67293aef2234b43aff8728d9002eb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Apr 2010 18:30:38 +0200 Subject: [PATCH 0149/3747] More documentation updates :) --- docs/patterns/packages.rst | 113 +++++++++++++++++++++++++++++++++++++ flask.py | 16 ++++++ 2 files changed, 129 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 4d54e49c..65678a15 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -17,6 +17,9 @@ this:: login.html ... +Simple Packages +--------------- + To convert that into a larger one, just create a new folder `yourapplication` inside the existing one and move everything below it. Then rename `yourapplication.py` to `__init__.py`. (Make sure to delete @@ -71,6 +74,20 @@ And this is what `views.py` would look like:: def index(): return 'Hello World!' +You should then end up with something like that:: + + /yourapplication + /yourapplication + /__init__.py + /views.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + .. admonition:: Circular Imports Every Python programmer hates them, and yet we just added some: @@ -84,3 +101,99 @@ And this is what `views.py` would look like:: There are still some problems with that approach but if you want to use decorators there is no way around that. Check out the :ref:`becomingbig` section for some inspiration how to deal with that. + + +.. _working-with-modules: + +Working with Modules +-------------------- + +For larger applications with more than a dozen views it makes sense to +split the views into module. First let's look at the typical struture of +such an application:: + + /yourapplication + /yourapplication + /__init__.py + /views + __init__.py + admin.py + frontend.py + /static + /style.css + /templates + layout.html + index.html + login.html + ... + +The views are stored in the `yourapplication.views` package. Just make +sure to place an empty `__init__.py` file in there. Let's start with the +`admin.py` file in the view package. + +First we have to create a :class:`~flask.Module` object with the name of +the package. This works very similar to the :class:`~flask.Flask` object +you have already worked with, it just does not support all of the method, +but most of them are the same. + +Long story short, here a nice and concise example:: + + from flask import Module + + admin = Module(__name__) + + @admin.route('/') + def index(): + pass + + @admin.route('/login') + def login(): + pass + + @admin.route('/logout') + def login(): + pass + +Do the same with the `frontend.py` and then make sure to register the +modules in the application (`__init__.py`) like this:: + + from flask import Flask + from yourapplication.views.admin import admin + from yourapplication.views.frontend import frontend + + app = Flask(__name__) + app.register_module(admin) + app.register_module(frontend) + +So what is different when working with modules? It mainly affects URL +generation. Remember the :func:`~flask.url_for` function? When not +working with modules it accepts the name of the function as first +argument. This first argument is called the "endpoint". When you are +working with modules you can use the name of the function like you did +without, when generating modules from a function or template in the same +module. If you want to generate the URL to another module, prefix it with +the name of the module and a dot. + +Confused? Let's clear that up with some examples. Imagine you have a +method in one module (say `admin`) and you want to redirect to a +different module (say `frontend`). This would look like this:: + + @admin.route('/to_frontend') + def to_frontend(): + return redirect(url_for('frontend.index')) + + @frontend.route('/') + def index(): + return "I'm the frontend index" + +Now let's say we only want to redirect to a different module in the same +module. Then we can either use the full qualified endpoint name like we +did in the example above, or we just use the function name:: + + @frontend.route('/to_index') + def to_index(): + return redirect(url_for('index')) + + @frontend.route('/') + def index(): + return "I'm the index" diff --git a/flask.py b/flask.py index 1442650c..0993e6a0 100644 --- a/flask.py +++ b/flask.py @@ -143,6 +143,19 @@ class _RequestContext(object): def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. + The endpoint is relative to the active module if modules are in use. + + Here some examples: + + ==================== ======================= ============================= + Active Module Target Endpoint Target Function + ==================== ======================= ============================= + `None` ``'index'`` `index` of the application + `None` ``'.index'`` `index` of the application + ``'admin'`` ``'index'`` `index` of the `admin` module + any ``'.index'`` `index` of the application + any ``'admin.index'`` `index` of the `admin` module + ==================== ======================= ============================= :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule @@ -382,6 +395,9 @@ class Module(_PackageBoundObject): @admin.route('/login') def login(): pass + + For a gentle introduction into modules, checkout the + :ref:`working-with-modules` section. """ def __init__(self, import_name, name=None, url_prefix=None): From 0c54d0a0e5dfec25cd60329e09622d42905d0969 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Apr 2010 20:36:06 +0200 Subject: [PATCH 0150/3747] Added pattern for file uploads. --- docs/patterns/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 8122deb7..b0b5fb72 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -16,6 +16,7 @@ end of the request, the database connection is closed again. packages sqlite3 sqlalchemy + fileuploads wtforms templateinheritance flashing From 85d43079d137c3f15e3ed7a9545ba2db92718194 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 03:46:42 +0200 Subject: [PATCH 0151/3747] Added missing file (fileupload docs) --- docs/patterns/fileuploads.rst | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/patterns/fileuploads.rst diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst new file mode 100644 index 00000000..fd94605d --- /dev/null +++ b/docs/patterns/fileuploads.rst @@ -0,0 +1,158 @@ +Uploading Files +=============== + +Ah yes, the good old problem of file uploads. The basic idea of file +uploads is actually quite simple. It basically works like this: + +1. A ```` tag is marked with ``enctype=multipart/form-data`` + and an ```` is placed in that form. +2. The application accesses the file from the :attr:`~flask.request.files` + dictionary on the request object. +3. use the :meth:`~werkzeug.FileStorage.save` method of the file to save + the file permanently somewhere on the filesystem. + +A Gentle Introduction +--------------------- + +Let's start with a very basic application that uploads a file to a +specific upload folder and displays a file to the user. Let's look at the +bootstrapping code for our application:: + + import os + from flask import Flask, request, redirect, url_for + from werkzeug import secure_filename + + UPLOAD_FOLDER = '/path/to/the/uploads' + ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) + + app = Flask(__name__) + app.add_url_rule('/uploads/', 'uploaded_file', + build_only=True) + +So first we need a couple of imports. Most should be straightforward, the +:func:`werkzeug.secure_filename` is explained a little bit later. The +`UPLOAD_FOLDER` is where we will store the uploaded files and the +`ALLOWED_EXTENSIONS` is the set of allowed file extensions. Then we add a +URL rule by hand to the application. Now usually we're not doing that, so +why here? The reasons is that we want the webserver (or our development +server) to serve these files for us and so we only need a rule to generate +the URL to these files. + +Why do we limit the extensions that are allowed? You probably don't want +your users to be able to upload everything there if the server is directly +sending out the data to the client. That way you can make sure that users +are not able to upload HTML files that would cause XSS problems. Also +make sure to disallow `.php` files if the server executes them, but who +has PHP installed on his server, right? :) + +Next the functions that check if an extension is valid and that uploads +the file and redirects the user to the URL for the uploaded file:: + + def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + + @app.route('/') + def upload_file(): + if request.method == 'POST': + file = request.files['file'] + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file.save(os.path.join(UPLOAD_FOLDER, filename)) + return redirect(url_for('uploaded_file', + filename=filename)) + return ''' + + Upload new File +

    Upload new File

    + +

    + +

    + ''' + +So what does that :func:`~werkzeug.secure_filename` function actually do? +Now the problem is that there is that principle called "never trust user +input". This is also true for the filename of an uploaded file. All +submitted form data can be forged, and filenames can be dangerous. For +the moment just remember: always use that function to secure a filename +before storing it directly on the filesystem. + +.. admonition:: Information for the Pros + + So you're interested in what that :func:`~werkzeug.secure_filename` + function does and what the problem is if you're not using it? So just + imagine someone would send the following information as `filename` to + your application:: + + filename = "../../../../home/username/.bashrc" + + Assuming the number of ``../`` is correct and you would join this with + the `UPLOAD_FOLDER` the user might have the ability to modify a file on + the server's filesystem he should not modify. This does require some + knowledge about how the application looks like, but trust me, hackers + are patient :) + + Now let's look how that function works: + + >>> secure_filename('../../../../home/username/.bashrc') + 'home_username_.bashrc' + +Now if we run that application, you will notice that uploading works, but +you won't actually see that uploaded file. Well, you would have to +configure the server to serve that file for you. This is not handy for +development situations or when you are just too lazy to properly set up +the server. Would be nice to have the files still be available in that +situation, and that is really easy to do, just hook in a middleware:: + + from werkzeug import SharedDataMiddleware + app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { + '/uploads': UPLOAD_FOLDER + }) + +If you now run the application everything should work as expected. + + +Improving Uploads +----------------- + +So how exactly does Flask handle uploads? Well it will store them in the +webserver's memory if the files are reasonable small otherwise in a +temporary location (as returned by :func:`tempfile.gettempdir`). But how +do you specify the maximum file size after which an upload is aborted? By +default Flask will happily accept file uploads to an unlimited amount of +memory, but you can limit that by subclassing the request and overriding +the Werkzeug provided :attr:`~werkzeug.BaseRequest.max_form_memory_size` +attribute:: + + from flask import Flask, Request + + class LimitedRequest(Request): + max_form_memory_size = 16 * 1024 * 1024 + + app = Flask(__name__) + app.request_class = LimitedRequest + +The code above will limited the maximum allowed payload to 16 megabytes. +If a larger file is transmitted, Flask will raise an +:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. + + +Upload Progress Bars +-------------------- + +A while ago many developers had the idea to read the incoming file in +small chunks and store the upload progress in the database to be able to +poll the progress with JavaScript from the client. Long story short: the +client asks the server every 5 seconds how much he has transmitted +already. Do you realize the irony? The client is asking for something he +should already know. + +Now there are better solutions to that work faster and more reliable. The +web changed a lot lately and you can use HTML5, Java, Silverlight or Flash +to get a nicer uploading experience on the client side. Look at the +following libraries for some nice examples how to do that: + +- `Plupload `_ - HTML5, Java, Flash +- `SWFUpload `_ - Flash +- `JumpLoader `_ - Java From 58fa088abc0f0e511d934e822f917b796d418bb3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 03:46:42 +0200 Subject: [PATCH 0152/3747] Added missing file (fileupload docs) --- docs/patterns/fileuploads.rst | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/patterns/fileuploads.rst diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst new file mode 100644 index 00000000..fd94605d --- /dev/null +++ b/docs/patterns/fileuploads.rst @@ -0,0 +1,158 @@ +Uploading Files +=============== + +Ah yes, the good old problem of file uploads. The basic idea of file +uploads is actually quite simple. It basically works like this: + +1. A ``
    `` tag is marked with ``enctype=multipart/form-data`` + and an ```` is placed in that form. +2. The application accesses the file from the :attr:`~flask.request.files` + dictionary on the request object. +3. use the :meth:`~werkzeug.FileStorage.save` method of the file to save + the file permanently somewhere on the filesystem. + +A Gentle Introduction +--------------------- + +Let's start with a very basic application that uploads a file to a +specific upload folder and displays a file to the user. Let's look at the +bootstrapping code for our application:: + + import os + from flask import Flask, request, redirect, url_for + from werkzeug import secure_filename + + UPLOAD_FOLDER = '/path/to/the/uploads' + ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) + + app = Flask(__name__) + app.add_url_rule('/uploads/', 'uploaded_file', + build_only=True) + +So first we need a couple of imports. Most should be straightforward, the +:func:`werkzeug.secure_filename` is explained a little bit later. The +`UPLOAD_FOLDER` is where we will store the uploaded files and the +`ALLOWED_EXTENSIONS` is the set of allowed file extensions. Then we add a +URL rule by hand to the application. Now usually we're not doing that, so +why here? The reasons is that we want the webserver (or our development +server) to serve these files for us and so we only need a rule to generate +the URL to these files. + +Why do we limit the extensions that are allowed? You probably don't want +your users to be able to upload everything there if the server is directly +sending out the data to the client. That way you can make sure that users +are not able to upload HTML files that would cause XSS problems. Also +make sure to disallow `.php` files if the server executes them, but who +has PHP installed on his server, right? :) + +Next the functions that check if an extension is valid and that uploads +the file and redirects the user to the URL for the uploaded file:: + + def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + + @app.route('/') + def upload_file(): + if request.method == 'POST': + file = request.files['file'] + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file.save(os.path.join(UPLOAD_FOLDER, filename)) + return redirect(url_for('uploaded_file', + filename=filename)) + return ''' + + Upload new File +

    Upload new File

    + +

    + +

    + ''' + +So what does that :func:`~werkzeug.secure_filename` function actually do? +Now the problem is that there is that principle called "never trust user +input". This is also true for the filename of an uploaded file. All +submitted form data can be forged, and filenames can be dangerous. For +the moment just remember: always use that function to secure a filename +before storing it directly on the filesystem. + +.. admonition:: Information for the Pros + + So you're interested in what that :func:`~werkzeug.secure_filename` + function does and what the problem is if you're not using it? So just + imagine someone would send the following information as `filename` to + your application:: + + filename = "../../../../home/username/.bashrc" + + Assuming the number of ``../`` is correct and you would join this with + the `UPLOAD_FOLDER` the user might have the ability to modify a file on + the server's filesystem he should not modify. This does require some + knowledge about how the application looks like, but trust me, hackers + are patient :) + + Now let's look how that function works: + + >>> secure_filename('../../../../home/username/.bashrc') + 'home_username_.bashrc' + +Now if we run that application, you will notice that uploading works, but +you won't actually see that uploaded file. Well, you would have to +configure the server to serve that file for you. This is not handy for +development situations or when you are just too lazy to properly set up +the server. Would be nice to have the files still be available in that +situation, and that is really easy to do, just hook in a middleware:: + + from werkzeug import SharedDataMiddleware + app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { + '/uploads': UPLOAD_FOLDER + }) + +If you now run the application everything should work as expected. + + +Improving Uploads +----------------- + +So how exactly does Flask handle uploads? Well it will store them in the +webserver's memory if the files are reasonable small otherwise in a +temporary location (as returned by :func:`tempfile.gettempdir`). But how +do you specify the maximum file size after which an upload is aborted? By +default Flask will happily accept file uploads to an unlimited amount of +memory, but you can limit that by subclassing the request and overriding +the Werkzeug provided :attr:`~werkzeug.BaseRequest.max_form_memory_size` +attribute:: + + from flask import Flask, Request + + class LimitedRequest(Request): + max_form_memory_size = 16 * 1024 * 1024 + + app = Flask(__name__) + app.request_class = LimitedRequest + +The code above will limited the maximum allowed payload to 16 megabytes. +If a larger file is transmitted, Flask will raise an +:exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. + + +Upload Progress Bars +-------------------- + +A while ago many developers had the idea to read the incoming file in +small chunks and store the upload progress in the database to be able to +poll the progress with JavaScript from the client. Long story short: the +client asks the server every 5 seconds how much he has transmitted +already. Do you realize the irony? The client is asking for something he +should already know. + +Now there are better solutions to that work faster and more reliable. The +web changed a lot lately and you can use HTML5, Java, Silverlight or Flash +to get a nicer uploading experience on the client side. Look at the +following libraries for some nice examples how to do that: + +- `Plupload `_ - HTML5, Java, Flash +- `SWFUpload `_ - Flash +- `JumpLoader `_ - Java From a9bb965b6dcd931cefdc7dd05fa4c672b5dff69c Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 03:44:11 +0800 Subject: [PATCH 0153/3747] add a decorator to add custom template filter --- flask.py | 22 ++++++++++++++++++++++ tests/flask_tests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/flask.py b/flask.py index 10feaf22..6d557b26 100644 --- a/flask.py +++ b/flask.py @@ -12,6 +12,7 @@ from __future__ import with_statement import os import sys +import types from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ @@ -639,6 +640,27 @@ class Flask(object): return f return decorator + def template_filter(self, arg=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + if type(arg) is types.FunctionType: + self.jinja_env.filters[arg.__name__] = arg + return arg + + def decorator(f): + self.jinja_env.filters[arg or f.__name__] = f + return f + return decorator + def before_request(self, f): """Registers a function to run before each request.""" self.before_request_funcs.append(f) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 917f4168..91edb9c2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -311,6 +311,32 @@ class TemplatingTestCase(unittest.TestCase): macro = flask.get_template_attribute('_macro.html', 'hello') assert macro('World') == 'Hello World!' + def test_template_filter_not_called(self): + app = flask.Flask(__name__) + @app.template_filter + def my_reverse(s): + return s[::-1] + assert 'my_reverse' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['my_reverse'] == my_reverse + assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + + def test_template_filter_called(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + assert 'my_reverse' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['my_reverse'] == my_reverse + assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + assert 'strrev' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['strrev'] == my_reverse + assert app.jinja_env.filters['strrev']('abcd') == 'dcba' def suite(): from minitwit_tests import MiniTwitTestCase From 5c9ef2c44dce5afc99a0b3b539ebaafe9b8ce3c5 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 17:39:19 +0800 Subject: [PATCH 0154/3747] the template_filter now expects the parentheses --- flask.py | 10 +++------- tests/flask_tests.py | 11 +---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/flask.py b/flask.py index 6d557b26..216f9af8 100644 --- a/flask.py +++ b/flask.py @@ -640,24 +640,20 @@ class Flask(object): return f return decorator - def template_filter(self, arg=None): + def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: - @app.template_filter + @app.template_filter() def reverse(s): return s[::-1] :param name: the optional name of the filter, otherwise the function name will be used. """ - if type(arg) is types.FunctionType: - self.jinja_env.filters[arg.__name__] = arg - return arg - def decorator(f): - self.jinja_env.filters[arg or f.__name__] = f + self.jinja_env.filters[name or f.__name__] = f return f return decorator diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 91edb9c2..6f5a4d38 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -311,16 +311,7 @@ class TemplatingTestCase(unittest.TestCase): macro = flask.get_template_attribute('_macro.html', 'hello') assert macro('World') == 'Hello World!' - def test_template_filter_not_called(self): - app = flask.Flask(__name__) - @app.template_filter - def my_reverse(s): - return s[::-1] - assert 'my_reverse' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['my_reverse'] == my_reverse - assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' - - def test_template_filter_called(self): + def test_template_filter(self): app = flask.Flask(__name__) @app.template_filter() def my_reverse(s): From 4395e9493cc33d49b53889f9f726458f05f1a475 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 25 Apr 2010 20:44:24 +0800 Subject: [PATCH 0155/3747] add tests for template_filter using a real template --- tests/flask_tests.py | 23 +++++++++++++++++++++++ tests/templates/template_filter.html | 1 + 2 files changed, 24 insertions(+) create mode 100644 tests/templates/template_filter.html diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6f5a4d38..b976015a 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -329,6 +329,29 @@ class TemplatingTestCase(unittest.TestCase): assert app.jinja_env.filters['strrev'] == my_reverse assert app.jinja_env.filters['strrev']('abcd') == 'dcba' + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase diff --git a/tests/templates/template_filter.html b/tests/templates/template_filter.html new file mode 100644 index 00000000..af46cd94 --- /dev/null +++ b/tests/templates/template_filter.html @@ -0,0 +1 @@ +{{ value|super_reverse }} \ No newline at end of file From 3ac4ec670c881668aaedb2c3937bae5e10984988 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 17:05:25 +0200 Subject: [PATCH 0156/3747] Beefed up PDF docs. They actually look like something now. --- docs/conf.py | 30 +++++++----------- docs/contents.rst | 41 +++++++++++++++++++++++++ docs/flaskstyle.sty | 73 ++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 42 +------------------------ docs/latexindex.rst | 4 +++ docs/logo.pdf | Bin 0 -> 28884 bytes 6 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 docs/contents.rst create mode 100644 docs/flaskstyle.sty create mode 100644 docs/latexindex.rst create mode 100644 docs/logo.pdf diff --git a/docs/conf.py b/docs/conf.py index 03e27217..f4756ae0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -172,35 +172,27 @@ htmlhelp_basename = 'Flaskdoc' # -- Options for LaTeX output -------------------------------------------------- -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Flask.tex', u'Flask Documentation', + ('latexindex', 'Flask.tex', u'Flask Documentation', u'Armin Ronacher', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +latex_use_modindex = False + +latex_elements = { + 'fontpkg': r'\usepackage{mathpazo}', + 'papersize': 'a4paper', + 'pointsize': '12pt', + 'preamble': r'\usepackage{flaskstyle}' +} + +latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] # -- Options for Epub output --------------------------------------------------- diff --git a/docs/contents.rst b/docs/contents.rst new file mode 100644 index 00000000..72491049 --- /dev/null +++ b/docs/contents.rst @@ -0,0 +1,41 @@ +User's Guide +------------ + +This part of the documentation is written text and should give you an idea +how to work with Flask. It's a series of step-by-step instructions for +web development. + +.. toctree:: + :maxdepth: 2 + + foreword + installation + quickstart + tutorial/index + testing + patterns/index + deploying/index + becomingbig + +Additional Notes +---------------- + +Design notes, legal information and changelog are here for the interested: + +.. toctree:: + :maxdepth: 2 + + design + license + changelog + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you: + +.. toctree:: + :maxdepth: 2 + + api diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty new file mode 100644 index 00000000..71d9ccc7 --- /dev/null +++ b/docs/flaskstyle.sty @@ -0,0 +1,73 @@ +\pagenumbering{arabic} +\definecolor{TitleColor}{rgb}{0,0,0} +\definecolor{InnerLinkColor}{rgb}{0,0,0} + +\renewcommand{\maketitle}{% + \begin{titlepage}% + \let\footnotesize\small + \let\footnoterule\relax + \ifsphinxpdfoutput + \begingroup + % This \def is required to deal with multi-line authors; it + % changes \\ to ', ' (comma-space), making it pass muster for + % generating document info in the PDF file. + \def\\{, } + \pdfinfo{ + /Author (\@author) + /Title (\@title) + } + \endgroup + \fi + \begin{flushright}% + %\sphinxlogo% + {\center + \vspace*{3cm} + \includegraphics{logo.pdf} + \vspace{3cm} + \par + {\rm\Huge \@title \par}% + {\em\LARGE \py@release\releaseinfo \par} + {\large + \@date \par + \py@authoraddress \par + }}% + \end{flushright}%\par + \@thanks + \end{titlepage}% + \cleardoublepage% + \setcounter{footnote}{0}% + \let\thanks\relax\let\maketitle\relax + %\gdef\@thanks{}\gdef\@author{}\gdef\@title{} +} + +\fancypagestyle{normal}{ + \fancyhf{} + \fancyfoot[LE,RO]{{\thepage}} + \fancyfoot[LO]{{\nouppercase{\rightmark}}} + \fancyfoot[RE]{{\nouppercase{\leftmark}}} + \fancyhead[LE,RO]{{ \@title, \py@release}} + \renewcommand{\headrulewidth}{0.4pt} + \renewcommand{\footrulewidth}{0.4pt} +} + +\fancypagestyle{plain}{ + \fancyhf{} + \fancyfoot[LE,RO]{{\thepage}} + \renewcommand{\headrulewidth}{0pt} + \renewcommand{\footrulewidth}{0.4pt} +} + +\titleformat{\section}{\Large}% + {\py@TitleColor\thesection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\subsection}{\large}% + {\py@TitleColor\thesubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\subsubsection}{}% + {\py@TitleColor\thesubsubsection}{0.5em}{\py@TitleColor}{\py@NormalColor} +\titleformat{\paragraph}{\large}% + {\py@TitleColor}{0em}{\py@TitleColor}{\py@NormalColor} + +\ChNameVar{\raggedleft\normalsize} +\ChNumVar{\raggedleft \bfseries\Large} +\ChTitleVar{\raggedleft \rm\Huge} + +\usepackage{inconsolata} diff --git a/docs/index.rst b/docs/index.rst index 1f3b6460..e7ab260b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,44 +25,4 @@ following links: .. _Jinja2: http://jinja.pocoo.org/2/ .. _Werkzeug: http://werkzeug.pocoo.org/ -User's Guide ------------- - -This part of the documentation is written text and should give you an idea -how to work with Flask. It's a series of step-by-step instructions for -web development. - -.. toctree:: - :maxdepth: 2 - - foreword - installation - quickstart - tutorial/index - testing - patterns/index - deploying/index - becomingbig - -Additional Notes ----------------- - -Design notes, legal information and changelog are here for the interested: - -.. toctree:: - :maxdepth: 2 - - design - license - changelog - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you: - -.. toctree:: - :maxdepth: 2 - - api +.. include:: contents.rst diff --git a/docs/latexindex.rst b/docs/latexindex.rst new file mode 100644 index 00000000..be54f2de --- /dev/null +++ b/docs/latexindex.rst @@ -0,0 +1,4 @@ +Flask Documentation +=================== + +.. include:: contents.rst diff --git a/docs/logo.pdf b/docs/logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..49c5060e2563a6a47986c7a4694f04e859c31dc4 GIT binary patch literal 28884 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 literal 0 HcmV?d00001 From d22428bc8e8c3cf5e6006497cb8164decde31924 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 17:08:44 +0200 Subject: [PATCH 0157/3747] Updated Makefile to also upload and build pdf docs. --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c62cf587..c9f0a113 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,7 @@ clean-pyc: find . -name '*~' -exec rm -f {} + upload-docs: - $(MAKE) -C docs dirhtml && scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ + $(MAKE) -C docs dirhtml latex + $(MAKE) -C docs/_build/latex all-pdf + scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ + scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf From 606b006e2391d12fec5313ce606d82463d7aad07 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 17:25:16 +0200 Subject: [PATCH 0158/3747] Fixed at least one build warning by renaming files. --- Makefile | 4 +++- docs/{contents.rst => contents.rst.inc} | 0 docs/index.rst | 2 +- docs/latexindex.rst | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) rename docs/{contents.rst => contents.rst.inc} (100%) diff --git a/Makefile b/Makefile index c9f0a113..94ad0077 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,9 @@ clean-pyc: find . -name '*~' -exec rm -f {} + upload-docs: - $(MAKE) -C docs dirhtml latex + $(MAKE) -C docs html dirhtml latex $(MAKE) -C docs/_build/latex all-pdf + cd docs/_build/; mv html flask-docs; zip -r flask-docs.zip flask-docs; mv flask-docs html scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf + scp -r docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/ diff --git a/docs/contents.rst b/docs/contents.rst.inc similarity index 100% rename from docs/contents.rst rename to docs/contents.rst.inc diff --git a/docs/index.rst b/docs/index.rst index e7ab260b..2c3159c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,4 +25,4 @@ following links: .. _Jinja2: http://jinja.pocoo.org/2/ .. _Werkzeug: http://werkzeug.pocoo.org/ -.. include:: contents.rst +.. include:: contents.rst.inc diff --git a/docs/latexindex.rst b/docs/latexindex.rst index be54f2de..0b611b45 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -1,4 +1,4 @@ Flask Documentation =================== -.. include:: contents.rst +.. include:: contents.rst.inc From f419937d2c5f2db16a8157a0a061b92c38876649 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 17:33:56 +0200 Subject: [PATCH 0159/3747] License after API docs --- docs/contents.rst.inc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 72491049..1e9e7df9 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,6 +17,17 @@ web development. deploying/index becomingbig +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you: + +.. toctree:: + :maxdepth: 2 + + api + Additional Notes ---------------- @@ -28,14 +39,3 @@ Design notes, legal information and changelog are here for the interested: design license changelog - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you: - -.. toctree:: - :maxdepth: 2 - - api From 4559e4f0f14272fb4fb31f3bf79bf592a9f781df Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Apr 2010 18:55:01 +0200 Subject: [PATCH 0160/3747] Added docs on caching and decorators. --- docs/patterns/caching.rst | 69 ++++++++++++++++++++++++ docs/patterns/index.rst | 2 + docs/patterns/viewdecorators.rst | 93 ++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 docs/patterns/caching.rst create mode 100644 docs/patterns/viewdecorators.rst diff --git a/docs/patterns/caching.rst b/docs/patterns/caching.rst new file mode 100644 index 00000000..7a811b82 --- /dev/null +++ b/docs/patterns/caching.rst @@ -0,0 +1,69 @@ +.. _caching-pattern: + +Caching +======= + +When your application runs slow, throw some caches in. Well, at least +it's the easiest way to speed up things. What does a cache do? Say you +have a function that takes some time to complete but the results would +still be good enough if they were 5 minutes old. So then the idea is that +you actually put the result of that calculation into a cache for some +time. + +Flask itself does not provide caching for you, but Werkzeug, one of the +libraries it is based on, has some very basic cache support. It supports +multiple cache backends, normally you want to use a memcached server. + +Setting up a Cache +------------------ + +You create a cache object once and keep it around, similar to how +:class:`~flask.Flask` objects are created. If you are using the +development server you can create a +:class:`~werkzeug.contrib.cache.SimpleCache` object, that one is a simple +cache that keeps the item stored in the memory of the Python interpreter:: + + from werkzeug.contrib.cache import SimpleCache + cache = SimpleCache() + +If you want to use memcached, make sure to have one of the memcache modules +supported (you get them from `PyPI `_) and a +memcached server running somewhere. This is how you connect to such an +memcached server then:: + + from werkzeug.contrib.cache import MemcachedCache + cache = MemcachedCache(['127.0.0.1:11211']) + +If you are using appengine, you can connect to the appengine memcache +server easily:: + + from werkzeug.contrib.cache import GAEMemcachedCache + cache = GAEMemcachedCache() + +Using a Cache +------------- + +Now how can one use such a cache? There are two very important +operations: :meth:`~werkzeug.contrib.cache.BaseCache.get` and +:meth:`~werkzeug.contrib.cache.BaseCache.set`. This is how to use them: + +To get an item from the cache call +:meth:`~werkzeug.contrib.cache.BaseCache.get` with a string as key name. +If something is in the cache, it is returned. Otherwise that function +will return `None`:: + + rv = cache.get('my-item') + +To add items to the cache, use the :meth:`~werkzeug.contrib.cache.BaseCache.set` +method instead. The first argument is the key and the second the value +that should be set. Also a timeout can be provided after which the cache +will automatically remove item. + +Here a full example how this looks like normally:: + + def get_my_item(): + rv = cache.get('my-item') + if rv is None: + rv = calculate_value() + cache.set('my-item', rv, timeout=5 * 60) + return rv diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index b0b5fb72..513647c9 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -17,6 +17,8 @@ end of the request, the database connection is closed again. sqlite3 sqlalchemy fileuploads + caching + viewdecorators wtforms templateinheritance flashing diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst new file mode 100644 index 00000000..22da533c --- /dev/null +++ b/docs/patterns/viewdecorators.rst @@ -0,0 +1,93 @@ +View Decorators +=============== + +Python has a really interesting feature called function decorators. This +allow some really neat things for web applications. Because each view in +Flask is a function decorators can be used to inject additional +functionality to one or more functions. The :meth:`~flask.Flask.route` +decorator is the one you probably used already. But there are use cases +for implementing your own decorator. For instance, imagine you have a +view that should only be used by people that are logged in to. If a user +goes to the site and is not logged in, he should be redirected to the +login page. This is a good example of a use case where a decorator is an +excellent solution. + +Login Required Decorator +------------------------ + +So let's implement such a decorator. A decorator is a function that +returns a function. Pretty simple actually. The only thing you have to +keep in mind when implementing something like this is to update the +`__name__`, `__module__` and some other attributes of a function. This is +often forgotten, but you don't have to do that by hand, there is a +function for that that is used like a decorator (:func:`functools.wraps`). + +This example assumes that the login page is called ``'login'`` and that +the current user is stored as `g.user` and `None` if there is no-one +logged in:: + + from functools import wraps + from flask import g, request, redirect, url_for + + def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user is None: + return redirect(url_for('login', next=request.url)) + return f(*args, **kwargs) + return decorated_function + +So how would you use that decorator now? Apply it as innermost decorator +to a view function. When applying further decorators, always remember +that the :meth:`~flask.Flask.route` decorator is the outermost:: + + @app.route('/secret_page') + @login_required + def secret_page(): + pass + +Caching Decorator +----------------- + +Imagine you have a view function that does an expensive calculation and +because of that you would like to cache the generated results for a +certain amount of time. A decorator would be nice for that. We're +assuming you have set up a cache like mentioned in :ref:`caching-pattern`. + +Here an example cache function. It generates the cache key from a +specific prefix (actually a format string) and the current path of the +request. Notice that we are using a function that first creates the +decorator that then decorates the function. Sounds awful? Unfortunately +it is a little bit more complex, but the code should still be +straightforward to read. + +The decorated function will then work as follows + +1. get the unique cache key for the current request base on the current + path. +2. get the value for that key from the cache. If the cache returned + something we will return that value. +3. otherwise the original function is called and the return value is + stored in the cache for the timeout provided (by default 5 minutes). + +Here the code:: + + from functools import wraps + from flask import request + + def cached(timeout=5 * 60, key='view/%s'): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + cache_key = key % request.path + rv = cache.get(cache_key) + if rv is not None: + return rv + rv = f(*args, **kwargs) + cache.set(cache_key, rv, timeout=timeout) + return rv + return decorated_function + return decorator + +Notice that this assumes an instanciated `cache` object is available, see +:ref:`caching-pattern` for more information. From 2ba88eefb54f76fc974181babac07ffc7c1f7e73 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 26 Apr 2010 09:51:02 +0800 Subject: [PATCH 0161/3747] Fixed simple typo --- docs/patterns/wtforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index bbceee8a..d62c5bd3 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -42,7 +42,7 @@ In the view function, the usage of this form looks like this:: form.password.data) db_session.add(user) flash('Thanks for registering') - redirect(url_for('login')) + return redirect(url_for('login')) return render_template('register.html', form=form) Notice that we are implying that the view is using SQLAlchemy here From 36717b02731ad7d86ba8effbe27e70405e48cd9b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Apr 2010 14:32:09 +0200 Subject: [PATCH 0162/3747] Added support for long running sessions. This closes #16. --- Makefile | 2 +- docs/api.rst | 7 +++++++ flask.py | 31 +++++++++++++++++++++++++++---- tests/flask_tests.py | 27 +++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 94ad0077..62d763d2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test +.PHONY: clean-pyc test upload-docs all: clean-pyc test diff --git a/docs/api.rst b/docs/api.rst index d285dbfd..3961dc99 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -174,6 +174,13 @@ To access the current session you can use the :class:`session` object: # so mark it as modified yourself session.modified = True + .. attribute:: permanent + + If set to `True` the session life for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to `False` (which is the default) the + session will be deleted when the user closes the browser. + Application Globals ------------------- diff --git a/flask.py b/flask.py index 216f9af8..32b690ca 100644 --- a/flask.py +++ b/flask.py @@ -13,6 +13,7 @@ from __future__ import with_statement import os import sys import types +from datetime import datetime, timedelta from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ @@ -86,7 +87,20 @@ class _RequestGlobals(object): pass -class _NullSession(SecureCookie): +class Session(SecureCookie): + """Expands the session for support for switching between permanent + and non-permanent sessions. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + def _set_permanent(self, value): + self['_permanent'] = bool(value) + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + +class _NullSession(Session): """Class used to generate nicer error messages if sessions are not available. Will still allow read-only access to the empty session but fail on setting. @@ -317,6 +331,11 @@ class Flask(object): #: The secure cookie uses this for the name of the session cookie session_cookie_name = 'session' + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + permanent_session_lifetime = timedelta(days=31) + #: options that are passed directly to the Jinja2 environment jinja_options = ImmutableDict( autoescape=True, @@ -493,8 +512,8 @@ class Flask(object): """ key = self.secret_key if key is not None: - return SecureCookie.load_cookie(request, self.session_cookie_name, - secret_key=key) + return Session.load_cookie(request, self.session_cookie_name, + secret_key=key) def save_session(self, session, response): """Saves the session if it needs updates. For the default @@ -505,7 +524,11 @@ class Flask(object): object) :param response: an instance of :attr:`response_class` """ - session.save_cookie(response, self.session_cookie_name) + expires = None + if session.permanent: + expires = datetime.utcnow() + self.permanent_session_lifetime + session.save_cookie(response, self.session_cookie_name, + expires=expires, httponly=True) def add_url_rule(self, rule, endpoint, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b976015a..1759f8b9 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -11,11 +11,14 @@ """ from __future__ import with_statement import os +import re import sys import flask import unittest import tempfile import warnings +from datetime import datetime +from werkzeug import parse_date example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -118,6 +121,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + assert expires.year == expected.year + assert expires.month == expected.month + assert expires.day == expected.day + + permanent = False + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + assert match is None + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From 31493850de3d7765d09419aa49b98089f79da3c2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 11:32:14 +0200 Subject: [PATCH 0163/3747] Fixed typo in flask quickstart docs. This fixes #21 --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6d641d26..4be9a63f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -452,7 +452,7 @@ transmitted in a `POST` or `PUT` request) you can use the :attr:`~flask.request.form` attribute. Here a full example of the two attributes mentioned above:: - @app.route('/login', method=['POST', 'GET']) + @app.route('/login', methods=['POST', 'GET']) def login(): error = None if request.method == 'POST': From f1603d33f266ab24eda604f76632fa604b91e3f9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 11:36:42 +0200 Subject: [PATCH 0164/3747] Docs mention query args now. This fixes #20 --- docs/quickstart.rst | 7 ++++++- flask.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4be9a63f..59e36bcc 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -160,6 +160,8 @@ The following converters exist: `path` like the default but also accepts slashes =========== =========================================== +.. _url-building: + URL Building ```````````` @@ -167,7 +169,8 @@ If it can match URLs, can it also generate them? Of course you can. To build a URL to a specific function you can use the :func:`~flask.url_for` function. It accepts the name of the function as first argument and a number of keyword arguments, each corresponding to the variable part of -the URL rule. Here some examples: +the URL rule. Unknown variable parts are appended to the URL as query +parameter. Here some examples: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -184,9 +187,11 @@ the URL rule. Here some examples: ... print url_for('index') ... print url_for('login') ... print url_for('profile', username='John Doe') +... print url_for('login', next='/') ... / /login +/login?next=/ /user/John%20Doe (This also uses the :meth:`~flask.Flask.test_request_context` method diff --git a/flask.py b/flask.py index 32b690ca..c1c3fdb7 100644 --- a/flask.py +++ b/flask.py @@ -145,6 +145,10 @@ class _RequestContext(object): def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. + + For more information, head over to the :ref:`Quickstart `. :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule From a9284afde97c30d136e8ec675794c7cdcf4ca277 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 11:38:40 +0200 Subject: [PATCH 0165/3747] Fixed typo in tutorial. This fixes #19 --- docs/tutorial/testing.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index 3d1aa806..c3075e3a 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -2,8 +2,7 @@ Bonus: Testing the Application =============================== Now that you have finished the application and everything works as -expected, it's probably not the best idea to add automated tests to -simplify modifications in the future. The application above is used as a -basic example of how to perform unittesting in the :ref:`testing` section -of the documentation. Go there to see how easy it is to test Flask -applications. +expected, it's probably not a good idea to add automated tests to simplify +modifications in the future. The application above is used as a basic +example of how to perform unittesting in the :ref:`testing` section of the +documentation. Go there to see how easy it is to test Flask applications. From 8d49440d8b881b2df2c10085a58d3a4e4085147e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 11:45:40 +0200 Subject: [PATCH 0166/3747] Added example for context bound objects to the testing docs. This fixes #18 --- docs/testing.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index 0901792b..be72e746 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -195,3 +195,22 @@ suite. .. _MiniTwit Example: http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ + + +Other Testing Tricks +-------------------- + +Besides using the test client we used above there is also the +:meth:`~flask.Flask.test_request_context` method that in combination with +the `with` statement can be used to activate a request context +temporarily. With that you can access the :class:`~flask.request`, +:class:`~flask.g` and :class:`~flask.session` objects like in view +functions. Here a full example that showcases this:: + + app = flask.Flask(__name__) + + with app.test_request_context('/?name=Peter'): + assert flask.request.path == '/' + assert flask.request.args['name'] == 'Peter' + +All the other objects that are context bound can be used the same. From a7266ffb90d109f94da96e9eea5f5bb4ff8ddc71 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 12:08:17 +0200 Subject: [PATCH 0167/3747] Added @templated decorator to the patterns. --- docs/patterns/viewdecorators.rst | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 22da533c..f777a294 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -91,3 +91,52 @@ Here the code:: Notice that this assumes an instanciated `cache` object is available, see :ref:`caching-pattern` for more information. + + +Templating Decorator +-------------------- + +A common pattern invented by the TurboGears guys a while back is a +templating decorator. The idea of that decorator is that you return a +dictionary with the values passed to the template from the view function +and the template is automatically rendered. With that, the following +three examples do exactly the same:: + + @app.route('/') + def index(): + return render_template('index.html', value=42) + + @app.route('/') + @templated('index.html') + def index(): + return dict(value=42) + + @app.route('/') + @templated() + def index(): + return dict(value=42) + +As you can see, if no template name is provided it will use the endpoint +of the URL map + ``'.html'``. Otherwise the provided template name is +used. When the decorated function returns, the dictionary returned is +passed to the template rendering function. If `None` is returned, an +empty dictionary is assumed. + +Here the code for that decorator:: + + from functools import wraps + from flask import request + + def templated(template=None): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + template_name = template + if template_name is None: + template_name = request.endpoint + '.html' + ctx = f(*args, **kwargs) + if ctx is None: + ctx = {} + return render_template(template_name, **ctx) + return decorated_function + return decorator From 7e1db5978c3c7aedcb43d7e2036edc85b0a22a6b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 12:11:04 +0200 Subject: [PATCH 0168/3747] Added module support to templated decorator. --- docs/patterns/viewdecorators.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index f777a294..49620ff8 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -117,10 +117,10 @@ three examples do exactly the same:: return dict(value=42) As you can see, if no template name is provided it will use the endpoint -of the URL map + ``'.html'``. Otherwise the provided template name is -used. When the decorated function returns, the dictionary returned is -passed to the template rendering function. If `None` is returned, an -empty dictionary is assumed. +of the URL map with dots converted to slashes + ``'.html'``. Otherwise +the provided template name is used. When the decorated function returns, +the dictionary returned is passed to the template rendering function. If +`None` is returned, an empty dictionary is assumed. Here the code for that decorator:: @@ -133,7 +133,8 @@ Here the code for that decorator:: def decorated_function(*args, **kwargs): template_name = template if template_name is None: - template_name = request.endpoint + '.html' + template_name = request.endpoint \ + .replace('.', '/') + '.html' ctx = f(*args, **kwargs) if ctx is None: ctx = {} From 75461c1467bbb2d83ee787bb13f4a338a4b8ad3e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 May 2010 19:10:44 +0200 Subject: [PATCH 0169/3747] Added _external support to url_for --- CHANGES | 1 + flask.py | 5 ++++- tests/flask_tests.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ce7289da..c3489d27 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Version 0.2 - :meth:`~flask.Flask.add_url_rule` can now also register a view function. - server listens on 127.0.0.1 by default now to fix issues with chrome. +- added external URL support. Version 0.1 ----------- diff --git a/flask.py b/flask.py index c1c3fdb7..a50b95be 100644 --- a/flask.py +++ b/flask.py @@ -152,8 +152,11 @@ def url_for(endpoint, **values): :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule + :param _external: if set to `True`, an absolute URL is generated. """ - return _request_ctx_stack.top.url_adapter.build(endpoint, values) + external = values.pop('_external', False) + return _request_ctx_stack.top.url_adapter.build(endpoint, values, + force_external=external) def get_template_attribute(template_name, attribute): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1759f8b9..f9369e36 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -227,6 +227,8 @@ class BasicFunctionalityTestCase(unittest.TestCase): pass with app.test_request_context(): assert flask.url_for('hello', name='test x') == '/hello/test%20x' + assert flask.url_for('hello', name='test x', _external=True) \ + == 'http://localhost/hello/test%20x' def test_custom_converters(self): from werkzeug.routing import BaseConverter From d0eefb01d16bd058172363a9847b5d98ccd1abc9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 3 May 2010 12:22:27 +0200 Subject: [PATCH 0170/3747] Added link to snippets website to the patterns docs. --- docs/patterns/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 513647c9..9678e3be 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -10,6 +10,9 @@ changes are they will open a database connection at the beginning of the request and get the information of the currently logged in user. At the end of the request, the database connection is closed again. +There are more user contributed snippets and patterns in the `Flask +Snippet Archives `_. + .. toctree:: :maxdepth: 2 From 9248a7baca5bc616a60f9b63b525e872fbfddef2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 3 May 2010 13:07:30 +0200 Subject: [PATCH 0171/3747] None responses fail properly now. This fixes #24 --- flask.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flask.py b/flask.py index a50b95be..c1229d6e 100644 --- a/flask.py +++ b/flask.py @@ -752,10 +752,7 @@ class Flask(object): :param rv: the return value from the view function """ if rv is None: - from warnings import warn - warn(Warning('View function did not return a response'), - stacklevel=2) - return u'' + raise ValueError('View function did not return a response') if isinstance(rv, self.response_class): return rv if isinstance(rv, basestring): From dffb3d091c0e42b7254fe88218811c24c0054f7e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 3 May 2010 14:39:16 +0200 Subject: [PATCH 0172/3747] Removed useless import and adapted tests --- flask.py | 1 - tests/flask_tests.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/flask.py b/flask.py index c1229d6e..02014fd2 100644 --- a/flask.py +++ b/flask.py @@ -12,7 +12,6 @@ from __future__ import with_statement import os import sys -import types from datetime import datetime, timedelta from jinja2 import Environment, PackageLoader, FileSystemLoader diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f9369e36..f8f74264 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -255,17 +255,17 @@ class BasicFunctionalityTestCase(unittest.TestCase): == '/static/index.html' def test_none_response(self): - warnings.filterwarnings('error', 'View function did not return') app = flask.Flask(__name__) @app.route('/') def test(): return None try: app.test_client().get('/') - except Warning: + except ValueError, e: + assert str(e) == 'View function did not return a response' pass else: - assert "Expected warning" + assert "Expected ValueError" class JSONTestCase(unittest.TestCase): From f345af8d9d33e6164747c865c29a72186a470c22 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 3 May 2010 19:58:07 +0200 Subject: [PATCH 0173/3747] Use unicode in SQLAlchemy pattern. --- docs/patterns/sqlalchemy.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 32d41c08..3945c1fa 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -25,7 +25,7 @@ Here the example `database.py` module for your application:: from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.ext.declarative import declarative_base - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) @@ -104,7 +104,7 @@ Here is an example `database.py` module for your application:: from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) metadata = MetaData() db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, @@ -156,7 +156,7 @@ you basically only need the engine:: from sqlalchemy import create_engine, MetaData - engine = create_engine('sqlite:////tmp/test.db') + engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) metadata = MetaData(bind=engine) Then you can either declare the tables in your code like in the examples From 67fc46526234653b0a519e9f1e8a05a2c40c80f8 Mon Sep 17 00:00:00 2001 From: florentx Date: Tue, 4 May 2010 01:38:25 +0800 Subject: [PATCH 0174/3747] Fix typo, remove useless import, limit lines to 79 columns. --- flask.py | 6 ++++-- tests/flask_tests.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/flask.py b/flask.py index 02014fd2..28becb3f 100644 --- a/flask.py +++ b/flask.py @@ -87,14 +87,16 @@ class _RequestGlobals(object): class Session(SecureCookie): - """Expands the session for support for switching between permanent + """Expands the session with support for switching between permanent and non-permanent sessions. """ def _get_permanent(self): return self.get('_permanent', False) + def _set_permanent(self, value): self['_permanent'] = bool(value) + permanent = property(_get_permanent, _set_permanent) del _get_permanent, _set_permanent @@ -391,7 +393,7 @@ class Flask(object): #: decorator. self.after_request_funcs = [] - #: a list of functions that are called without arguments + #: a list of functions that are called without argument #: to populate the template context. Each returns a dictionary #: that the template context is updated with. #: To register a function here, use the :meth:`context_processor` diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f8f74264..1daf0d4b 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -16,7 +16,6 @@ import sys import flask import unittest import tempfile -import warnings from datetime import datetime from werkzeug import parse_date @@ -61,7 +60,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert sorted(rv.allow) == ['GET', 'HEAD'] rv = c.head('/') assert rv.status_code == 200 - assert not rv.data # head truncates + assert not rv.data # head truncates assert c.post('/more').data == 'POST' assert c.get('/more').data == 'GET' rv = c.delete('/more') @@ -85,7 +84,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert sorted(rv.allow) == ['GET', 'HEAD'] rv = c.head('/') assert rv.status_code == 200 - assert not rv.data # head truncates + assert not rv.data # head truncates assert c.post('/more').data == 'POST' assert c.get('/more').data == 'GET' rv = c.delete('/more') @@ -191,7 +190,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): flask.abort(404) @app.route('/error') def error(): - 1/0 + 1 // 0 c = app.test_client() rv = c.get('/') assert rv.status_code == 404 @@ -236,7 +235,8 @@ class BasicFunctionalityTestCase(unittest.TestCase): def to_python(self, value): return value.split(',') def to_url(self, value): - return ','.join(super(ListConverter, self).to_url(x) for x in value) + base_to_url = super(ListConverter, self).to_url + return ','.join(base_to_url(x) for x in value) app = flask.Flask(__name__) app.url_map.converters['list'] = ListConverter @app.route('/') @@ -297,10 +297,11 @@ class JSONTestCase(unittest.TestCase): def test_template_escaping(self): app = flask.Flask(__name__) + render = flask.render_template_string with app.test_request_context(): - rv = flask.render_template_string('{{ ""|tojson|safe }}') + rv = render('{{ ""|tojson|safe }}') assert rv == '"<\\/script>"' - rv = flask.render_template_string('{{ "<\0/script>"|tojson|safe }}') + rv = render('{{ "<\0/script>"|tojson|safe }}') assert rv == '"<\\u0000\\/script>"' From bc0c0559e384dc88441775a3907487f4a2b515b4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 May 2010 11:23:01 +0200 Subject: [PATCH 0175/3747] Added OpenID example --- examples/openidexample/openidexample.py | 165 ++++++++++++++++++ examples/openidexample/simpleopenid.py | 108 ++++++++++++ examples/openidexample/static/openid.png | Bin 0 -> 433 bytes examples/openidexample/static/style.css | 39 +++++ .../templates/create_profile.html | 22 +++ .../openidexample/templates/edit_profile.html | 16 ++ examples/openidexample/templates/index.html | 10 ++ examples/openidexample/templates/layout.html | 18 ++ examples/openidexample/templates/login.html | 12 ++ 9 files changed, 390 insertions(+) create mode 100644 examples/openidexample/openidexample.py create mode 100644 examples/openidexample/simpleopenid.py create mode 100644 examples/openidexample/static/openid.png create mode 100644 examples/openidexample/static/style.css create mode 100644 examples/openidexample/templates/create_profile.html create mode 100644 examples/openidexample/templates/edit_profile.html create mode 100644 examples/openidexample/templates/index.html create mode 100644 examples/openidexample/templates/layout.html create mode 100644 examples/openidexample/templates/login.html diff --git a/examples/openidexample/openidexample.py b/examples/openidexample/openidexample.py new file mode 100644 index 00000000..6a7301ea --- /dev/null +++ b/examples/openidexample/openidexample.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +""" + OpenID Example + ~~~~~~~~~~~~~~ + + This simple application shows how OpenID can be used in an application. + + Dependencies: + + - python-openid + - SQLAlchemy + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from flask import Flask, render_template, request, g, session, flash, \ + redirect, url_for, abort +from simpleopenid import SimpleOpenID + +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base + +# configuration +DATABASE_URI = 'sqlite:////tmp/openidexample.db' +OPENID_FS_PATH = '/tmp/openidexample-store' +SECRET_KEY = 'development key' +DEBUG = True + +# setup flask +app = Flask(__name__) +app.debug = DEBUG +app.secret_key = SECRET_KEY + +# setup simpleopenid +oid = SimpleOpenID(OPENID_FS_PATH) + +# setup sqlalchemy +engine = create_engine(DATABASE_URI) +db_session = scoped_session(sessionmaker(autocommit=False, + autoflush=False, + bind=engine)) +Base = declarative_base() +Base.query = db_session.query_property() + +def init_db(): + Base.metadata.create_all(bind=engine) + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(String(60)) + email = Column(String(200)) + openid = Column(String(200)) + + def __init__(self, name, email, openid): + self.name = name + self.email = email + self.openid = openid + + +@app.before_request +def before_request(): + g.user = None + if 'openid' in session: + g.user = User.query.filter_by(openid=session['openid']).first() + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/login', methods=['GET', 'POST']) +@oid.loginhandler +def login(): + """Does the login via OpenID. Has to call into `oid.try_login` + to start the OpenID machinery. + """ + # if we are already logged in, go back to were we came from + if g.user is not None: + return redirect(oid.get_next_url()) + if request.method == 'POST': + openid = request.form.get('openid') + if openid: + return oid.try_login(openid) + return render_template('login.html', next_url=oid.get_next_url()) + + +@oid.after_login +def create_or_login(identity_url): + """This is called when login with OpenID succeeded and it's not + necessary to figure out if this is the users's first login or not. + This function has to redirect otherwise the user will be presented + with a terrible URL which we certainly don't want. + """ + session['openid'] = identity_url + user = User.query.filter_by(openid=identity_url).first() + if user is not None: + flash(u'Successfully signed in') + g.user = user + return redirect(oid.get_next_url()) + return redirect(url_for('create_profile', next=oid.get_next_url())) + + +@app.route('/create-profile', methods=['GET', 'POST']) +def create_profile(): + """If this is the user's first login, the create_or_login function + will redirect here so that the user can set up his profile. + """ + if g.user is not None or 'openid' not in session: + return redirect(url_for('index')) + if request.method == 'POST': + name = request.form['name'] + email = request.form['email'] + if not name: + flash(u'Error: you have to provide a name') + elif '@' not in email: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + db_session.add(User(name, email, session['openid'])) + db_session.commit() + return redirect(oid.get_next_url()) + return render_template('create_profile.html', next_url=oid.get_next_url()) + + +@app.route('/profile', methods=['GET', 'POST']) +def edit_profile(): + """Updates a profile""" + if g.user is None: + abort(401) + form = dict(name=g.user.name, email=g.user.email) + if request.method == 'POST': + if 'delete' in request.form: + db_session.delete(g.user) + db_session.commit() + session['openid'] = None + flash(u'Profile deleted') + return redirect(url_for('index')) + form['name'] = request.form['name'] + form['email'] = request.form['email'] + if not form['name']: + flash(u'Error: you have to provide a name') + elif '@' not in form['email']: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + g.user.name = form['name'] + g.user.email = form['email'] + db_session.commit() + return redirect(url_for('edit_profile')) + return render_template('edit_profile.html', form=form) + + +@app.route('/logout') +def logout(): + session.pop('openid', None) + flash(u'You were signed out') + return redirect(oid.get_next_url()) + + +if __name__ == '__main__': + app.run() diff --git a/examples/openidexample/simpleopenid.py b/examples/openidexample/simpleopenid.py new file mode 100644 index 00000000..c8e86463 --- /dev/null +++ b/examples/openidexample/simpleopenid.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +""" + simpleopenid + ~~~~~~~~~~~~ + + Tiny wrapper around python-openid to make working with the basic + API in a flask application easier. Adapt this code for your own + project if necessary. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from functools import wraps + +from flask import request, session, flash, redirect +from werkzeug import url_quote + +from openid.association import Association +from openid.store.interface import OpenIDStore +from openid.store.filestore import FileOpenIDStore +from openid.consumer.consumer import Consumer, SUCCESS, CANCEL +from openid.consumer import discover +from openid.store import nonce + +# python-openid is a really stupid library in that regard, we have +# to disable logging by monkey patching +from openid import oidutil +oidutil.log = lambda *a, **kw: None + + +class SimpleOpenID(object): + """Simple helper class for OpenID auth.""" + + def __init__(self, store_path): + self.store_path = store_path + self.after_login_func = None + + def create_store(self): + """Creates the filesystem store""" + return FileOpenIDStore(self.store_path) + + def signal_error(self, msg): + """Signals an error. It does this by flashing a message""" + flash(u'Error: ' + msg) + + def get_next_url(self): + """Return the URL where we want to redirect to.""" + return request.values.get('next') or \ + request.referrer or \ + request.url_root + + def get_current_url(self): + """the current URL + next""" + return request.base_url + '?next=' + url_quote(self.get_next_url()) + + def get_success_url(self): + """Return the success URL""" + return self.get_current_url() + '&openid_complete=yes' + + def errorhandler(f): + """Called if an error occours with the message. By default + ``'Error: message'`` is flashed. + """ + self.signal_error = f + return f + + def after_login(self, f): + """This function will be called after login. It must redirect to + a different place and remember the user somewhere. The session + is not modified by SimpleOpenID. + """ + self.after_login_func = f + return f + + def loginhandler(self, f): + """Marks a function as login handler. This decorator injects some + more OpenID required logic. + """ + self.login_endpoint = f.__name__ + @wraps(f) + def decorated(*args, **kwargs): + if request.args.get('openid_complete') != u'yes': + return f(*args, **kwargs) + consumer = Consumer(session, self.create_store()) + openid_response = consumer.complete(request.args.to_dict(), + self.get_current_url()) + if openid_response.status == SUCCESS: + return self.after_login_func(openid_response.identity_url) + elif openid_response.status == CANCEL: + self.signal_error(u'The request was cancelled') + return redirect(self.get_current_url()) + self.signal_error(u'OpenID authentication error') + return redirect(self.get_current_url()) + return decorated + + def try_login(self, identity_url): + """This tries to login with the given identity URL. This function + must be called from the login_handler. + """ + try: + consumer = Consumer(session, self.create_store()) + auth_request = consumer.begin(identity_url) + except discover.DiscoveryFailure: + self.signal_error(u'The OpenID was invalid') + return redirect(self.get_current_url()) + trust_root = request.host_url + return redirect(auth_request.redirectURL(request.host_url, + self.get_success_url())) diff --git a/examples/openidexample/static/openid.png b/examples/openidexample/static/openid.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7954ab12080e1cfb0583669a58267bea008a44 GIT binary patch literal 433 zcmV;i0Z#sjP)DL1dJy@3D#0X|7Y zK~y-)wbMUH)^QL9@X!15!y(?mMM2P4p@xE{I+t9cA<2T^g}0^6DYpnD0w*VNtlcRZ zqPZNxAflj-7X_<9aESg4Qc-~~QK-vr5W&}8!-Io6?)yC4-E&`#+O{VRZBLrouU zd>!vFekcM=pw{U@^?ygK>`pSXz(rifnd6Jor+e3*)qd8`ZLVFpyNVVT@i-#>Waf!t z7r2RejNk{BiuSi&oGjYkKjQF~8cv)sk21Hd98k$YA6TgE+yQK5?aW&2SsvO9u^ zc!QfI$%D+C%gp|6IuH@}Z~@D>jZc~R^4H+#7}|L9Plr@W8n}eB_2+ZH`&J*_sVrDO bvfsJ`%6MX|2>A&`00000NkvXXu0mjf^=ZFi literal 0 HcmV?d00001 diff --git a/examples/openidexample/static/style.css b/examples/openidexample/static/style.css new file mode 100644 index 00000000..33d9816b --- /dev/null +++ b/examples/openidexample/static/style.css @@ -0,0 +1,39 @@ +body { + font-family: 'Georgia', serif; + font-size: 16px; + margin: 30px; + padding: 0; +} + +a { + color: #335E79; +} + +p.message { + color: #335E79; + padding: 10px; + background: #CADEEB; +} + +input { + font-family: 'Georgia', serif; + font-size: 16px; + border: 1px solid black; + color: #335E79; + padding: 2px; +} + +input[type="submit"] { + background: #CADEEB; + color: #335E79; + border-color: #335E79; +} + +input[name="openid"] { + background: url(openid.png) 4px no-repeat; + padding-left: 24px; +} + +h1, h2 { + font-weight: normal; +} diff --git a/examples/openidexample/templates/create_profile.html b/examples/openidexample/templates/create_profile.html new file mode 100644 index 00000000..73a469a2 --- /dev/null +++ b/examples/openidexample/templates/create_profile.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} +{% block title %}Create Profile{% endblock %} +{% block body %} +

    Create Profile

    +

    + Hey! This is the first time you signed in on this website. In + order to proceed we need a couple of more information from you: +

    +
    +
    Name: +
    +
    E-Mail +
    +
    +

    + + +

    +

    + If you don't want to proceed, you can sign out again. +{% endblock %} diff --git a/examples/openidexample/templates/edit_profile.html b/examples/openidexample/templates/edit_profile.html new file mode 100644 index 00000000..a9b6b877 --- /dev/null +++ b/examples/openidexample/templates/edit_profile.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} +{% block title %}Edit Profile{% endblock %} +{% block body %} +

    Edit Profile

    +
    +
    +
    Name: +
    +
    E-Mail +
    +
    +

    + + +

    +{% endblock %} diff --git a/examples/openidexample/templates/index.html b/examples/openidexample/templates/index.html new file mode 100644 index 00000000..54f5161b --- /dev/null +++ b/examples/openidexample/templates/index.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block body %} +

    Overview

    + {% if g.user %} +

    + Hello {{ g.user.name }}! + {% endif %} +

    + This is just an example page so that something is here. +{% endblock %} diff --git a/examples/openidexample/templates/layout.html b/examples/openidexample/templates/layout.html new file mode 100644 index 00000000..3fad7c07 --- /dev/null +++ b/examples/openidexample/templates/layout.html @@ -0,0 +1,18 @@ + +{% block title %}Welcome{% endblock %} | Flask OpenID Example + +

    Flask OpenID Example

    + +{% for message in get_flashed_messages() %} +

    {{ message }} +{% endfor %} +{% block body %}{% endblock %} diff --git a/examples/openidexample/templates/login.html b/examples/openidexample/templates/login.html new file mode 100644 index 00000000..9c7c8ade --- /dev/null +++ b/examples/openidexample/templates/login.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} +{% block title %}Sign in{% endblock %} +{% block body %} +

    Sign in

    +
    +

    + OpenID: + + + +

    +{% endblock %} From a921aef6c47cecbacef5173f03c07c52e6bc8fea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 May 2010 11:41:54 +0200 Subject: [PATCH 0176/3747] Fixed late binding of url_prefix. This fixes #29. --- flask.py | 2 +- tests/flask_tests.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 268dec38..1d0fa852 100644 --- a/flask.py +++ b/flask.py @@ -448,7 +448,7 @@ class Module(_PackageBoundObject): """ def register_rule(state): the_rule = rule - if self.url_prefix: + if state.url_prefix: the_rule = state.url_prefix + rule state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), view_func, **options) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index d57370f8..8b3f7640 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -441,6 +441,15 @@ class ModuleTestCase(unittest.TestCase): assert catched == ['before-app', 'before-admin', 'after-admin', 'after-app'] + def test_late_binding(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.route('/') + def index(): + return '42' + app.register_module(admin, url_prefix='/admin') + assert app.test_client().get('/admin/').data == '42' + def suite(): from minitwit_tests import MiniTwitTestCase From f6da77e894a939b838e02fb744fa3031abfc96ec Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 May 2010 11:43:19 +0200 Subject: [PATCH 0177/3747] Fixed URL examples. This fixes #30. --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 3961dc99..ac761565 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -104,8 +104,8 @@ Incoming Request Data ============= ====================================================== `path` ``/page.html`` `script_root` ``/myapplication`` - `url` ``http://www.example.com/myapplication/page.html`` - `base_url` ``http://www.example.com/myapplication/page.html?x=y`` + `base_url` ``http://www.example.com/myapplication/page.html`` + `url` ``http://www.example.com/myapplication/page.html?x=y`` `url_root` ``http://www.example.com/myapplication/`` ============= ====================================================== From 745638e774063de206aae930df71789de13e7c73 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 May 2010 11:51:07 +0200 Subject: [PATCH 0178/3747] Added missing decorators for module wide context processors. This fixes #25. --- flask.py | 20 ++++++++++++++++++-- tests/flask_tests.py | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/flask.py b/flask.py index 1d0fa852..c5516c4f 100644 --- a/flask.py +++ b/flask.py @@ -465,7 +465,7 @@ class Module(_PackageBoundObject): def before_app_request(self, f): """Like :meth:`Flask.before_request`. Such a function is executed - before each request. + before each request, even if outside of a module. """ self._record(lambda s: s.app.before_request_funcs .setdefault(None, []).append(f)) @@ -482,12 +482,28 @@ class Module(_PackageBoundObject): def after_app_request(self, f): """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request. + is executed after each request, even if outside of the module. """ self._record(lambda s: s.app.after_request_funcs .setdefault(None, []).append(f)) return f + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a modul. This + function is only executed for requests handled by a module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. Such a + function is executed each request, even if outside of the module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + def _record(self, func): self._register_events.append(func) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 8b3f7640..1240dadd 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -441,6 +441,29 @@ class ModuleTestCase(unittest.TestCase): assert catched == ['before-app', 'before-admin', 'after-admin', 'after-app'] + def test_context_processors(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @app.context_processor + def inject_all_regualr(): + return {'a': 1} + @admin.context_processor + def inject_admin(): + return {'b': 2} + @admin.app_context_processor + def inject_all_module(): + return {'c': 3} + @app.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + @admin.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + app.register_module(admin) + c = app.test_client() + assert c.get('/').data == '13' + assert c.get('/admin/').data == '123' + def test_late_binding(self): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin') From b93ec1465be95a8303cbed99589273f562928103 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 4 May 2010 16:41:04 +0200 Subject: [PATCH 0179/3747] Documented module changes in CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index c3489d27..ae523e86 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Version 0.2 view function. - server listens on 127.0.0.1 by default now to fix issues with chrome. - added external URL support. +- module support and internal request handling refactoring + to better support pluggable applications. Version 0.1 ----------- From da8f88a22eab8195e48fa4ef63c6a3195c860eb0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 6 May 2010 13:32:52 +0200 Subject: [PATCH 0180/3747] Removed OpenID example, that's now in flaskext.openid anyways. --- examples/openidexample/openidexample.py | 165 ------------------ examples/openidexample/simpleopenid.py | 108 ------------ examples/openidexample/static/openid.png | Bin 433 -> 0 bytes examples/openidexample/static/style.css | 39 ----- .../templates/create_profile.html | 22 --- .../openidexample/templates/edit_profile.html | 16 -- examples/openidexample/templates/index.html | 10 -- examples/openidexample/templates/layout.html | 18 -- examples/openidexample/templates/login.html | 12 -- 9 files changed, 390 deletions(-) delete mode 100644 examples/openidexample/openidexample.py delete mode 100644 examples/openidexample/simpleopenid.py delete mode 100644 examples/openidexample/static/openid.png delete mode 100644 examples/openidexample/static/style.css delete mode 100644 examples/openidexample/templates/create_profile.html delete mode 100644 examples/openidexample/templates/edit_profile.html delete mode 100644 examples/openidexample/templates/index.html delete mode 100644 examples/openidexample/templates/layout.html delete mode 100644 examples/openidexample/templates/login.html diff --git a/examples/openidexample/openidexample.py b/examples/openidexample/openidexample.py deleted file mode 100644 index 6a7301ea..00000000 --- a/examples/openidexample/openidexample.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -""" - OpenID Example - ~~~~~~~~~~~~~~ - - This simple application shows how OpenID can be used in an application. - - Dependencies: - - - python-openid - - SQLAlchemy - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from flask import Flask, render_template, request, g, session, flash, \ - redirect, url_for, abort -from simpleopenid import SimpleOpenID - -from sqlalchemy import create_engine, Column, Integer, String -from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.ext.declarative import declarative_base - -# configuration -DATABASE_URI = 'sqlite:////tmp/openidexample.db' -OPENID_FS_PATH = '/tmp/openidexample-store' -SECRET_KEY = 'development key' -DEBUG = True - -# setup flask -app = Flask(__name__) -app.debug = DEBUG -app.secret_key = SECRET_KEY - -# setup simpleopenid -oid = SimpleOpenID(OPENID_FS_PATH) - -# setup sqlalchemy -engine = create_engine(DATABASE_URI) -db_session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=engine)) -Base = declarative_base() -Base.query = db_session.query_property() - -def init_db(): - Base.metadata.create_all(bind=engine) - - -class User(Base): - __tablename__ = 'users' - id = Column(Integer, primary_key=True) - name = Column(String(60)) - email = Column(String(200)) - openid = Column(String(200)) - - def __init__(self, name, email, openid): - self.name = name - self.email = email - self.openid = openid - - -@app.before_request -def before_request(): - g.user = None - if 'openid' in session: - g.user = User.query.filter_by(openid=session['openid']).first() - - -@app.route('/') -def index(): - return render_template('index.html') - - -@app.route('/login', methods=['GET', 'POST']) -@oid.loginhandler -def login(): - """Does the login via OpenID. Has to call into `oid.try_login` - to start the OpenID machinery. - """ - # if we are already logged in, go back to were we came from - if g.user is not None: - return redirect(oid.get_next_url()) - if request.method == 'POST': - openid = request.form.get('openid') - if openid: - return oid.try_login(openid) - return render_template('login.html', next_url=oid.get_next_url()) - - -@oid.after_login -def create_or_login(identity_url): - """This is called when login with OpenID succeeded and it's not - necessary to figure out if this is the users's first login or not. - This function has to redirect otherwise the user will be presented - with a terrible URL which we certainly don't want. - """ - session['openid'] = identity_url - user = User.query.filter_by(openid=identity_url).first() - if user is not None: - flash(u'Successfully signed in') - g.user = user - return redirect(oid.get_next_url()) - return redirect(url_for('create_profile', next=oid.get_next_url())) - - -@app.route('/create-profile', methods=['GET', 'POST']) -def create_profile(): - """If this is the user's first login, the create_or_login function - will redirect here so that the user can set up his profile. - """ - if g.user is not None or 'openid' not in session: - return redirect(url_for('index')) - if request.method == 'POST': - name = request.form['name'] - email = request.form['email'] - if not name: - flash(u'Error: you have to provide a name') - elif '@' not in email: - flash(u'Error: you have to enter a valid email address') - else: - flash(u'Profile successfully created') - db_session.add(User(name, email, session['openid'])) - db_session.commit() - return redirect(oid.get_next_url()) - return render_template('create_profile.html', next_url=oid.get_next_url()) - - -@app.route('/profile', methods=['GET', 'POST']) -def edit_profile(): - """Updates a profile""" - if g.user is None: - abort(401) - form = dict(name=g.user.name, email=g.user.email) - if request.method == 'POST': - if 'delete' in request.form: - db_session.delete(g.user) - db_session.commit() - session['openid'] = None - flash(u'Profile deleted') - return redirect(url_for('index')) - form['name'] = request.form['name'] - form['email'] = request.form['email'] - if not form['name']: - flash(u'Error: you have to provide a name') - elif '@' not in form['email']: - flash(u'Error: you have to enter a valid email address') - else: - flash(u'Profile successfully created') - g.user.name = form['name'] - g.user.email = form['email'] - db_session.commit() - return redirect(url_for('edit_profile')) - return render_template('edit_profile.html', form=form) - - -@app.route('/logout') -def logout(): - session.pop('openid', None) - flash(u'You were signed out') - return redirect(oid.get_next_url()) - - -if __name__ == '__main__': - app.run() diff --git a/examples/openidexample/simpleopenid.py b/examples/openidexample/simpleopenid.py deleted file mode 100644 index c8e86463..00000000 --- a/examples/openidexample/simpleopenid.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -""" - simpleopenid - ~~~~~~~~~~~~ - - Tiny wrapper around python-openid to make working with the basic - API in a flask application easier. Adapt this code for your own - project if necessary. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from functools import wraps - -from flask import request, session, flash, redirect -from werkzeug import url_quote - -from openid.association import Association -from openid.store.interface import OpenIDStore -from openid.store.filestore import FileOpenIDStore -from openid.consumer.consumer import Consumer, SUCCESS, CANCEL -from openid.consumer import discover -from openid.store import nonce - -# python-openid is a really stupid library in that regard, we have -# to disable logging by monkey patching -from openid import oidutil -oidutil.log = lambda *a, **kw: None - - -class SimpleOpenID(object): - """Simple helper class for OpenID auth.""" - - def __init__(self, store_path): - self.store_path = store_path - self.after_login_func = None - - def create_store(self): - """Creates the filesystem store""" - return FileOpenIDStore(self.store_path) - - def signal_error(self, msg): - """Signals an error. It does this by flashing a message""" - flash(u'Error: ' + msg) - - def get_next_url(self): - """Return the URL where we want to redirect to.""" - return request.values.get('next') or \ - request.referrer or \ - request.url_root - - def get_current_url(self): - """the current URL + next""" - return request.base_url + '?next=' + url_quote(self.get_next_url()) - - def get_success_url(self): - """Return the success URL""" - return self.get_current_url() + '&openid_complete=yes' - - def errorhandler(f): - """Called if an error occours with the message. By default - ``'Error: message'`` is flashed. - """ - self.signal_error = f - return f - - def after_login(self, f): - """This function will be called after login. It must redirect to - a different place and remember the user somewhere. The session - is not modified by SimpleOpenID. - """ - self.after_login_func = f - return f - - def loginhandler(self, f): - """Marks a function as login handler. This decorator injects some - more OpenID required logic. - """ - self.login_endpoint = f.__name__ - @wraps(f) - def decorated(*args, **kwargs): - if request.args.get('openid_complete') != u'yes': - return f(*args, **kwargs) - consumer = Consumer(session, self.create_store()) - openid_response = consumer.complete(request.args.to_dict(), - self.get_current_url()) - if openid_response.status == SUCCESS: - return self.after_login_func(openid_response.identity_url) - elif openid_response.status == CANCEL: - self.signal_error(u'The request was cancelled') - return redirect(self.get_current_url()) - self.signal_error(u'OpenID authentication error') - return redirect(self.get_current_url()) - return decorated - - def try_login(self, identity_url): - """This tries to login with the given identity URL. This function - must be called from the login_handler. - """ - try: - consumer = Consumer(session, self.create_store()) - auth_request = consumer.begin(identity_url) - except discover.DiscoveryFailure: - self.signal_error(u'The OpenID was invalid') - return redirect(self.get_current_url()) - trust_root = request.host_url - return redirect(auth_request.redirectURL(request.host_url, - self.get_success_url())) diff --git a/examples/openidexample/static/openid.png b/examples/openidexample/static/openid.png deleted file mode 100644 index ce7954ab12080e1cfb0583669a58267bea008a44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 433 zcmV;i0Z#sjP)DL1dJy@3D#0X|7Y zK~y-)wbMUH)^QL9@X!15!y(?mMM2P4p@xE{I+t9cA<2T^g}0^6DYpnD0w*VNtlcRZ zqPZNxAflj-7X_<9aESg4Qc-~~QK-vr5W&}8!-Io6?)yC4-E&`#+O{VRZBLrouU zd>!vFekcM=pw{U@^?ygK>`pSXz(rifnd6Jor+e3*)qd8`ZLVFpyNVVT@i-#>Waf!t z7r2RejNk{BiuSi&oGjYkKjQF~8cv)sk21Hd98k$YA6TgE+yQK5?aW&2SsvO9u^ zc!QfI$%D+C%gp|6IuH@}Z~@D>jZc~R^4H+#7}|L9Plr@W8n}eB_2+ZH`&J*_sVrDO bvfsJ`%6MX|2>A&`00000NkvXXu0mjf^=ZFi diff --git a/examples/openidexample/static/style.css b/examples/openidexample/static/style.css deleted file mode 100644 index 33d9816b..00000000 --- a/examples/openidexample/static/style.css +++ /dev/null @@ -1,39 +0,0 @@ -body { - font-family: 'Georgia', serif; - font-size: 16px; - margin: 30px; - padding: 0; -} - -a { - color: #335E79; -} - -p.message { - color: #335E79; - padding: 10px; - background: #CADEEB; -} - -input { - font-family: 'Georgia', serif; - font-size: 16px; - border: 1px solid black; - color: #335E79; - padding: 2px; -} - -input[type="submit"] { - background: #CADEEB; - color: #335E79; - border-color: #335E79; -} - -input[name="openid"] { - background: url(openid.png) 4px no-repeat; - padding-left: 24px; -} - -h1, h2 { - font-weight: normal; -} diff --git a/examples/openidexample/templates/create_profile.html b/examples/openidexample/templates/create_profile.html deleted file mode 100644 index 73a469a2..00000000 --- a/examples/openidexample/templates/create_profile.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Create Profile{% endblock %} -{% block body %} -

    Create Profile

    -

    - Hey! This is the first time you signed in on this website. In - order to proceed we need a couple of more information from you: -

    -
    -
    Name: -
    -
    E-Mail -
    -
    -

    - - -

    -

    - If you don't want to proceed, you can sign out again. -{% endblock %} diff --git a/examples/openidexample/templates/edit_profile.html b/examples/openidexample/templates/edit_profile.html deleted file mode 100644 index a9b6b877..00000000 --- a/examples/openidexample/templates/edit_profile.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Edit Profile{% endblock %} -{% block body %} -

    Edit Profile

    -
    -
    -
    Name: -
    -
    E-Mail -
    -
    -

    - - -

    -{% endblock %} diff --git a/examples/openidexample/templates/index.html b/examples/openidexample/templates/index.html deleted file mode 100644 index 54f5161b..00000000 --- a/examples/openidexample/templates/index.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

    Overview

    - {% if g.user %} -

    - Hello {{ g.user.name }}! - {% endif %} -

    - This is just an example page so that something is here. -{% endblock %} diff --git a/examples/openidexample/templates/layout.html b/examples/openidexample/templates/layout.html deleted file mode 100644 index 3fad7c07..00000000 --- a/examples/openidexample/templates/layout.html +++ /dev/null @@ -1,18 +0,0 @@ - -{% block title %}Welcome{% endblock %} | Flask OpenID Example - -

    Flask OpenID Example

    - -{% for message in get_flashed_messages() %} -

    {{ message }} -{% endfor %} -{% block body %}{% endblock %} diff --git a/examples/openidexample/templates/login.html b/examples/openidexample/templates/login.html deleted file mode 100644 index 9c7c8ade..00000000 --- a/examples/openidexample/templates/login.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Sign in{% endblock %} -{% block body %} -

    Sign in

    -
    -

    - OpenID: - - - -

    -{% endblock %} From 34e3d64892f2281aa1b6f6357c345b0a357ee51a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 May 2010 20:16:00 +0200 Subject: [PATCH 0181/3747] Removed theme, this is now refactored to an external reusable theme. --- docs/_themes/flasky/static/flasky.css_t | 344 ------------------------ docs/_themes/flasky/theme.conf | 3 - docs/conf.py | 7 +- 3 files changed, 2 insertions(+), 352 deletions(-) delete mode 100644 docs/_themes/flasky/static/flasky.css_t delete mode 100644 docs/_themes/flasky/theme.conf diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t deleted file mode 100644 index 04c5c5f6..00000000 --- a/docs/_themes/flasky/static/flasky.css_t +++ /dev/null @@ -1,344 +0,0 @@ -/* - * flasky.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- flasky theme based on nature theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: 'Georgia', serif; - font-size: 17px; - background-color: #ddd; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background: #fafafa; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; - min-height: 34em; -} - -img.floatingflask { - padding: 0 0 10px 10px; - float: right; -} - -div.footer { - position: absolute; - right: 0; - margin-top: -70px; - text-align: right; - color: #888; - padding: 10px; - font-size: 14px; -} - -div.footer a { - color: #888; - text-decoration: underline; -} - -div.related { - line-height: 32px; - color: #888; -} - -div.related ul { - padding: 0 0 0 10px; -} - -div.related a { - color: #444; -} - -div.sphinxsidebar { - font-size: 14px; - line-height: 1.5; -} - -div.sphinxsidebarwrapper { - padding: 0 20px; -} - -div.sphinxsidebarwrapper p.logo { - padding: 20px 0 10px 0; - margin: 0; - text-align: center; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: 'Garamond', 'Georgia', serif; - color: #222; - font-size: 24px; - font-weight: normal; - margin: 20px 0 5px 0; - padding: 0; -} - -div.sphinxsidebar h4 { - font-size: 20px; -} - -div.sphinxsidebar h3 a { - color: #444; -} - -div.sphinxsidebar p { - color: #555; - margin: 10px 0; -} - -div.sphinxsidebar ul { - margin: 10px 0; - padding: 0; - color: #000; -} - -div.sphinxsidebar a { - color: #444; - text-decoration: none; -} - -div.sphinxsidebar a:hover { - text-decoration: underline; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: 'Georgia', serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #004B6B; - text-decoration: underline; -} - -a:hover { - color: #6D4100; - text-decoration: underline; -} - -div.body { - padding-bottom: 40px; /* saved for footer */ -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - margin: 30px 0px 10px 0px; - padding: 0; -} - -div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } -div.body h2 { font-size: 180%; } -div.body h3 { font-size: 150%; } -div.body h4 { font-size: 130%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: white; - padding: 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - color: #444; - background: #eaeaea; -} - -div.body p, div.body dd, div.body li { - line-height: 1.4em; -} - -div.admonition { - background: #fafafa; - margin: 20px -30px; - padding: 10px 30px; - border-top: 1px solid #ccc; - border-bottom: 1px solid #ccc; -} - -div.admonition p.admonition-title { - font-family: 'Garamond', 'Georgia', serif; - font-weight: normal; - font-size: 24px; - margin: 0 0 10px 0; - padding: 0; - line-height: 1; -} - -div.admonition p.last { - margin-bottom: 0; -} - -div.highlight{ - background-color: white; -} - -dt:target, .highlight { - background: #FAF3E8; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre, tt { - font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; - font-size: 0.9em; -} - -img.screenshot { -} - -tt.descname, tt.descclassname { - font-size: 0.95em; -} - -tt.descname { - padding-right: 0.08em; -} - -img.screenshot { - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils { - border: 1px solid #888; - -moz-box-shadow: 2px 2px 4px #eee; - -webkit-box-shadow: 2px 2px 4px #eee; - box-shadow: 2px 2px 4px #eee; -} - -table.docutils td, table.docutils th { - border: 1px solid #888; - padding: 0.25em 0.7em; -} - -table.field-list, table.footnote { - border: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; - box-shadow: none; -} - -table.footnote { - margin: 15px 0; - width: 100%; - border: 1px solid #eee; -} - -table.field-list th { - padding: 0 0.8em 0 0; -} - -table.field-list td { - padding: 0; -} - -table.footnote td { - padding: 0.5em; -} - -dl { - margin: 0; - padding: 0; -} - -dl dd { - margin-left: 30px; -} - -pre { - background: #eee; - padding: 7px 30px; - margin: 15px -30px; - line-height: 1.3em; -} - -dl pre { - margin-left: -60px; - padding-left: 60px; -} - -dl dl pre { - margin-left: -90px; - padding-left: 90px; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ -} - -tt.xref, a tt { - background-color: #FBFBFB; -} - -a:hover tt { - background: #EEE; -} diff --git a/docs/_themes/flasky/theme.conf b/docs/_themes/flasky/theme.conf deleted file mode 100644 index cb9eb465..00000000 --- a/docs/_themes/flasky/theme.conf +++ /dev/null @@ -1,3 +0,0 @@ -[theme] -inherit = basic -stylesheet = flasky.css diff --git a/docs/conf.py b/docs/conf.py index f4756ae0..ff5951b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath('_themes')) # -- General configuration ----------------------------------------------------- @@ -79,9 +79,6 @@ exclude_patterns = ['_build'] # output. They are ignored by default. #show_authors = False -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'flaskext.FlaskyStyle' - # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] @@ -90,7 +87,7 @@ pygments_style = 'flaskext.FlaskyStyle' # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'flasky' +html_theme = 'flask' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 4156bd412f2c27359844fbacdae1eb4983c94b8b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 May 2010 20:23:38 +0200 Subject: [PATCH 0182/3747] added theme submodule --- .gitmodules | 3 +++ docs/_themes | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 docs/_themes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..bf7b494f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/_themes"] + path = docs/_themes + url = git://github.com/mitsuhiko/flask-sphinx-themes.git diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..11cb6b51 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 11cb6b51c9ea3bc8f94afa3d7411b617f9db2570 From 2d87e9bc37ef1ce8f9068fac92783a4fe3d3d382 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 10 May 2010 11:27:42 +0200 Subject: [PATCH 0183/3747] Added support for send_file --- CHANGES | 1 + docs/api.rst | 2 ++ flask.py | 75 +++++++++++++++++++++++++++++++++++++- tests/flask_tests.py | 85 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index c3489d27..b2f54fdc 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,7 @@ Version 0.2 view function. - server listens on 127.0.0.1 by default now to fix issues with chrome. - added external URL support. +- added support for :func:`~flask.send_file` Version 0.1 ----------- diff --git a/docs/api.rst b/docs/api.rst index ac761565..3e46dde4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -213,6 +213,8 @@ Useful Functions and Classes .. autofunction:: redirect +.. autofunction:: send_file + .. autofunction:: escape .. autoclass:: Markup diff --git a/flask.py b/flask.py index 28becb3f..3d1a8b7b 100644 --- a/flask.py +++ b/flask.py @@ -12,12 +12,13 @@ from __future__ import with_statement import os import sys +import mimetypes from datetime import datetime, timedelta from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property + ImmutableDict, cached_property, wrap_file, Headers from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException from werkzeug.contrib.securecookie import SecureCookie @@ -235,6 +236,71 @@ def jsonify(*args, **kwargs): indent=None if request.is_xhr else 2), mimetype='application/json') +def send_file(filename_or_fp, mimetype=None, as_attachment=False, + attachment_filename=None): + """Sends the contents of a file to the client. This will use the + most efficient method available and configured. By default it will + try to use the WSGI server's file_wrapper support. Alternatively + you can set the application's :attr:`~Flask.use_x_sendfile` attribute + to ``True`` to directly emit an `X-Sendfile` header. This however + requires support of the underlying webserver for `X-Sendfile`. + + By default it will try to guess the mimetype for you, but you can + also explicitly provide one. For extra security you probably want + to sent certain files as attachment (HTML for instance). + + .. versionadded:: 0.2 + + :param filename_or_fp: the filename of the file to send. This is + relative to the :attr:`~Flask.root_path` if a + relative path is specified. + Alternatively a file object might be provided + in which case `X-Sendfile` might not work and + fall back to the traditional method. + :param mimetype: the mimetype of the file if provided, otherwise + auto detection happens. + :param as_attachment: set to `True` if you want to send this file with + a ``Content-Disposition: attachment`` header. + :param attachment_filename: the filename for the attachment if it + differs from the file's filename. + """ + if isinstance(filename_or_fp, basestring): + filename = filename_or_fp + file = None + else: + file = filename_or_fp + filename = getattr(file, 'name', None) + if filename is not None: + filename = os.path.join(current_app.root_path, filename) + if mimetype is None and (filename or attachment_filename): + mimetype = mimetypes.guess_type(filename or attachment_filename)[0] + if mimetype is None: + mimetype = 'application/octet-stream' + + headers = Headers() + if as_attachment: + if attachment_filename is None: + if filename is None: + raise TypeError('filename unavailable, required for ' + 'sending as attachment') + attachment_filename = os.path.basename(filename) + headers.add('Content-Disposition', 'attachment', + filename=attachment_filename) + + if current_app.use_x_sendfile and filename: + if file is not None: + file.close() + headers['X-Sendfile'] = filename + data = None + else: + if file is None: + file = open(filename, 'rb') + data = wrap_file(request.environ, file) + + return Response(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -344,6 +410,13 @@ class Flask(object): #: permanent session survive for roughly one month. permanent_session_lifetime = timedelta(days=31) + #: Enable this if you want to use the X-Sendfile feature. Keep in + #: mind that the server has to support this. This only affects files + #: sent with the :func:`send_file` method. + #: + #: .. versionadded:: 0.2 + use_x_sendfile = False + #: options that are passed directly to the Jinja2 environment jinja_options = ImmutableDict( autoescape=True, diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1daf0d4b..29ae2762 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -17,7 +17,8 @@ import flask import unittest import tempfile from datetime import datetime -from werkzeug import parse_date +from werkzeug import parse_date, parse_options_header +from cStringIO import StringIO example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -382,6 +383,87 @@ class TemplatingTestCase(unittest.TestCase): assert rv.data == 'dcba' +class SendfileTestCase(unittest.TestCase): + + def test_send_file_regular(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.send_file('static/index.html') + assert rv.direct_passthrough + assert rv.mimetype == 'text/html' + with app.open_resource('static/index.html') as f: + assert rv.data == f.read() + + def test_send_file_xsendfile(self): + app = flask.Flask(__name__) + app.use_x_sendfile = True + with app.test_request_context(): + rv = flask.send_file('static/index.html') + assert rv.direct_passthrough + assert 'x-sendfile' in rv.headers + assert rv.headers['x-sendfile'] == \ + os.path.join(app.root_path, 'static/index.html') + assert rv.mimetype == 'text/html' + + def test_send_file_object(self): + app = flask.Flask(__name__) + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + assert rv.data == f.read() + assert rv.mimetype == 'text/html' + + app.use_x_sendfile = True + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + assert rv.mimetype == 'text/html' + assert 'x-sendfile' in rv.headers + assert rv.headers['x-sendfile'] == \ + os.path.join(app.root_path, 'static/index.html') + + app.use_x_sendfile = False + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + assert rv.data == 'Test' + assert rv.mimetype == 'application/octet-stream' + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + assert rv.data == 'Test' + assert rv.mimetype == 'text/plain' + + app.use_x_sendfile = True + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + assert 'x-sendfile' not in rv.headers + + def test_attachment(self): + app = flask.Flask(__name__) + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + + with app.test_request_context(): + assert options['filename'] == 'index.html' + rv = flask.send_file('static/index.html', as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + assert options['filename'] == 'index.html' + + with app.test_request_context(): + rv = flask.send_file(StringIO('Test'), as_attachment=True, + attachment_filename='index.txt') + assert rv.mimetype == 'text/plain' + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + assert options['filename'] == 'index.txt' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -389,6 +471,7 @@ def suite(): suite.addTest(unittest.makeSuite(ContextTestCase)) suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) suite.addTest(unittest.makeSuite(TemplatingTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From 5bb2b55a2880b3c91dbcd6580f3d25ee23c6655c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 10 May 2010 11:35:43 +0200 Subject: [PATCH 0184/3747] Explicitly set the pygments style for the PDF --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index ff5951b3..2048ab6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -234,3 +234,5 @@ intersphinx_mapping = { 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None } + +pygments_style = 'flask_theme_support.FlaskyStyle' From f80e1d3b5ab8e718cc67c814cbfd9aee9c95f45f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 12 May 2010 01:02:20 +0200 Subject: [PATCH 0185/3747] Added note on send_file security. --- flask.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask.py b/flask.py index 3d1a8b7b..ac33ba49 100644 --- a/flask.py +++ b/flask.py @@ -249,6 +249,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, also explicitly provide one. For extra security you probably want to sent certain files as attachment (HTML for instance). + Please never pass filenames to this function from user sources without + checking them first. Something like this is usually sufficient to + avoid security problems:: + + if '..' in filename or filename.startswith('/'): + abort(404) + .. versionadded:: 0.2 :param filename_or_fp: the filename of the file to send. This is From b0ab127015186e1a2918221ffad8d79ca346faf5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 12 May 2010 01:29:25 +0200 Subject: [PATCH 0186/3747] Added logo with new slogan --- CHANGES | 6 +- Makefile | 3 + artwork/logo-full.svg | 281 ++++++++++++++++--------------------- docs/_static/logo-full.png | Bin 23478 -> 20806 bytes setup.cfg | 6 + setup.py | 2 +- 6 files changed, 136 insertions(+), 162 deletions(-) mode change 100755 => 100644 artwork/logo-full.svg create mode 100644 setup.cfg diff --git a/CHANGES b/CHANGES index 485902e7..5c645435 100644 --- a/CHANGES +++ b/CHANGES @@ -6,18 +6,22 @@ Here you can see the full list of changes between each Flask release. Version 0.2 ----------- -[unreleased; current development version] +Released on May 12th, codename Jägermeister - various bugfixes - integrated JSON support - added :func:`~flask.get_template_attribute` helper function. - :meth:`~flask.Flask.add_url_rule` can now also register a view function. +- refactored internal request dispatching. - server listens on 127.0.0.1 by default now to fix issues with chrome. - added external URL support. - added support for :func:`~flask.send_file` - module support and internal request handling refactoring to better support pluggable applications. +- sessions can be set to be permanent now on a per-session basis. +- better error reporting on missing secret keys. +- added support for Google Appengine. Version 0.1 ----------- diff --git a/Makefile b/Makefile index 62d763d2..b46bb3b5 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ all: clean-pyc test test: python tests/flask_tests.py +release: + python setup.py release sdist upload + clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + diff --git a/artwork/logo-full.svg b/artwork/logo-full.svg old mode 100755 new mode 100644 index 43465a4d..8c0748a2 --- a/artwork/logo-full.svg +++ b/artwork/logo-full.svg @@ -141,166 +141,6 @@ style="fill:#000000" d="M 96.944917,182.03377 C 89.662681,176.30608 81.894549,170.81448 76.586317,163.08166 65.416842,149.44499 56.816875,133.6567 50.937585,117.06515 47.383955,106.27654 46.166898,94.709824 41.585799,84.338096 c -4.792287,-7.533044 0.821224,-15.767897 9.072722,-18.16242 3.673742,-0.705104 10.133327,-4.170258 2.335951,-1.693539 -6.990592,5.128871 -7.667129,-4.655603 -0.498823,-5.27517 4.892026,-0.650249 6.692895,-4.655044 5.019966,-8.260251 -5.251326,-3.424464 12.733737,-7.18801 3.684373,-12.297799 -9.426987,-10.170666 13.186339,-12.128546 7.607283,-0.577786 -1.335447,8.882061 15.801226,-1.627907 11.825117,8.628945 4.041283,4.925694 15.133562,1.1211 14.85838,8.031392 5.887092,0.404678 7.907562,5.358061 13.433992,5.738347 5.72759,2.586557 16.1108,4.624792 18.0598,11.079149 -5.68242,4.498756 -18.84089,-9.292674 -19.47305,3.160397 1.71659,18.396078 1.27926,37.346439 8.00986,54.864989 3.18353,10.60759 10.9012,18.95779 17.87109,27.21946 6.66875,8.09126 15.70186,13.78715 24.90885,18.58338 8.07647,3.80901 16.78383,6.33528 25.58583,7.92044 3.5701,-2.7307 9.87303,-12.8828 15.44238,-8.60188 0.26423,4.81007 -11.0541,10.05512 -0.53248,9.5235 6.17819,-1.86378 10.46336,4.77803 15.55099,-1.21289 4.68719,5.55206 19.48197,-3.54734 16.14693,7.80115 -4.50972,2.90955 -11.08689,1.15142 -15.60404,5.15397 -7.44757,-3.71979 -13.37691,3.32843 -21.6219,2.43707 -9.15641,1.64002 -18.4716,2.30204 -27.75473,2.31642 -15.22952,-1.20328 -30.78158,-1.71049 -45.26969,-7.01291 -8.16166,-2.37161 -16.12649,-7.01887 -23.299683,-11.66829 z m 12.862043,5.5729 c 7.9696,3.44651 15.76243,7.07889 24.49656,8.17457 13.85682,1.92727 28.16653,4.89163 42.07301,2.18757 -6.2939,-2.84199 -12.80077,1.10719 -19.07096,-2.0322 -7.52033,1.61821 -15.59049,-0.41223 -23.23574,-1.41189 -8.69395,-3.87259 -18.0762,-6.53549 -26.21772,-11.56219 -10.173155,-3.71578 5.26142,4.76524 8.00873,5.45214 6.35952,3.60969 -6.99343,-1.85044 -8.87589,-3.35101 -5.32648,-2.9879 -6.00529,-2.36357 -0.52745,0.67085 1.10332,0.64577 2.19359,1.32226 3.34946,1.87216 z M 94.642259,176.88976 c 7.722781,2.86052 -0.03406,-5.43082 -3.572941,-4.94904 -1.567906,-2.72015 -5.9903,-4.43854 -2.870721,-5.89973 -5.611524,1.9481 -5.878319,-7.40814 -8.516004,-6.07139 -5.936516,-1.87454 -2.310496,-8.51501 -9.381929,-12.59292 -0.645488,-4.29697 -7.02577,-8.02393 -9.060801,-14.50525 -0.898786,-3.31843 -7.208336,-12.84783 -3.332369,-3.97927 3.300194,8.53747 9.106618,15.84879 13.93868,23.15175 3.752083,6.95328 8.182497,14.22026 15.015767,18.55788 2.303436,2.20963 4.527452,5.59533 7.780318,6.28797 z M 72.39456,152.46355 c 0.26956,-1.16626 1.412424,2.52422 0,0 z m 31.49641,27.85526 c 1.71013,-0.76577 -2.45912,-0.96476 0,0 z m 4.19228,1.52924 c -0.43419,-2.1116 -1.91376,1.18074 0,0 z m 5.24749,2.18891 c 2.49828,-2.37871 -3.85009,-1.49983 0,0 z m 8.99389,5.01274 c 1.51811,-2.2439 -4.85872,-0.84682 0,0 z m -17.2707,-12.03933 c 3.88031,-2.51023 -5.01186,-0.0347 0,0 z m 3.9366,1.96293 c -0.11004,-1.32709 -1.40297,0.59432 0,0 z m 19.67473,12.28006 c 3.16281,1.99601 18.46961,4.3749 8.88477,0.81847 -1.60377,0.33811 -17.77263,-4.57336 -8.88477,-0.81847 z M 97.430958,166.92721 c -0.307503,-1.33094 -4.909341,-1.4694 0,0 z m 9.159302,5.33813 c 2.38371,-1.66255 -4.94757,-1.28235 0,0 z m 7.70426,4.72382 c 3.42065,-1.28963 -5.54907,-1.29571 0,0 z M 93.703927,162.86805 c 3.711374,2.84621 14.967683,0.36473 5.683776,-1.69906 -4.225516,-2.2524 -13.74889,-3.79415 -7.25757,1.35821 l 1.573785,0.34088 9e-6,-3e-5 z m 25.808723,15.75216 c 1.54595,-2.63388 -6.48298,-1.50411 0,0 z m -7.84249,-6.23284 c 9.0752,2.56719 -7.63142,-5.739 -2.23911,-0.94466 l 1.19513,0.54082 1.04399,0.4039 -1e-5,-6e-5 z m 15.72354,9.0878 c 8.59474,0.082 -7.76304,-1.18486 0,1e-5 l 0,-1e-5 z M 90.396984,157.89545 c -0.335695,-1.60094 -2.120962,0.13419 0,0 z m 51.535396,31.73502 c 0.2292,-2.89141 -2.80486,2.15157 0,0 z m -36.86817,-22.75299 c -0.51986,-1.52251 -2.68548,-0.0622 0,0 z m -13.852128,-9.98649 c 4.934237,-0.29629 -6.755322,-2.17418 0,0 z M 74.802387,146.28394 c -0.614146,-2.36536 -5.369213,-4.2519 0,0 z m 43.079323,27.33941 c -0.90373,-1.0307 -0.4251,0.22546 0,0 z m 26.81408,16.45475 c -0.086,-1.57503 -1.46039,0.59616 0,0 z m -29.18712,-18.90528 c 0.48266,-2.02932 -4.20741,-0.61442 0,0 z M 95.532612,158.51286 c 3.670785,-0.39305 -5.880434,-2.48161 0,0 z M 129.32396,179.51 c 5.72042,-2.26627 -5.57541,-1.10635 0,0 z m -17.57682,-11.93145 c 6.59278,0.85002 -7.84442,-4.48425 -1.44651,-0.4773 l 1.4465,0.47734 1e-5,-4e-5 z m 22.91296,14.0886 c 6.15514,-3.67975 4.12588,8.61677 10.44254,1.0388 6.23086,-4.54942 -5.38086,5.62451 2.29838,0.81116 5.55359,-3.71438 13.75643,1.76075 18.93848,3.5472 3.72659,-0.18307 7.34938,3.22236 11.16973,1.15059 7.3542,-1.98082 -14.38097,-2.93789 -8.68344,-6.4523 -6.72914,1.95848 -11.70093,-2.33483 -15.01213,-6.64508 -7.54812,-1.74298 -16.27548,-5.602 -20.04257,-12.28184 -1.5359,-2.50802 2.21884,0.35333 -1.32586,-3.74638 -4.54834,-4.04546 -6.81948,-8.63766 -9.87278,-13.5552 -3.64755,-1.94587 -4.07249,-7.67345 -4.44123,-0.19201 0.0289,-4.72164 -4.40393,-7.89964 -5.48589,-6.57859 -0.0194,-4.54721 4.74396,-2.26787 1.40945,-5.63228 -0.71771,-4.71302 -3.08085,-9.6241 -3.79115,-14.9453 -1.1036,-2.56502 -0.15541,-8.05863 -3.76662,-2.25204 -1.31566,6.13669 -0.43668,-7.54129 1.6093,-3.03083 2.68543,-4.60251 -0.9641,-4.0612 -1.11361,-3.42211 1.74931,-3.88333 1.10719,-9.39159 -0.45644,-7.29023 0.93213,-4.11586 1.47259,-15.147529 -1.3951,-13.192579 1.73833,-4.303958 3.29668,-19.694077 -4.24961,-13.826325 -3.058358,0.04294 -8.354541,1.110195 -10.858032,2.355243 7.849502,4.326857 -0.789543,1.562577 -3.984808,0.874879 -0.416343,4.003642 -3.58119,2.272086 -7.535123,2.311339 6.315273,0.781339 -3.075253,6.458962 -6.698132,4.253506 -4.705102,2.248756 4.060621,7.862038 0.0944,9.597586 0.487433,2.616581 -7.208227,-0.944906 -6.603832,5.097711 -4.56774,-1.92155 -0.628961,7.16796 1.656273,4.09382 7.768882,2.10261 5.469108,6.89631 5.666947,11.44992 -1.265833,2.6534 -6.249495,-6.23691 -1.109939,-5.82517 -4.054715,-6.58674 -4.485232,-2.38081 -7.854566,0.67911 -0.783857,0.22222 8.5944,4.35376 2.709059,6.3967 5.177884,0.79894 5.325199,5.33008 6.379284,8.19735 3.11219,3.24152 2.475226,-3.57931 6.199071,0.31623 -2.356488,-3.4705 -12.48183,-9.77839 -4.329567,-7.7553 -0.04358,-3.49291 -1.474412,-6.30951 1.02322,-6.24118 2.473367,-4.47926 -2.590385,11.044 2.984725,5.35124 1.543285,-0.67388 1.92554,-4.48494 4.699544,0.35989 4.029096,3.96363 1.45533,6.83577 -4.228162,3.20648 1.016828,3.44946 7.603062,4.68217 6.365348,10.07646 1.3121,4.7444 3.147844,2.99695 4.747999,2.72266 1.25523,4.60973 1.968016,1.2201 2.027559,-0.97355 5.747357,1.23033 4.401142,4.62773 6.199456,7.00134 3.960416,1.78761 -5.668696,-12.11713 1.130659,-4.18106 7.153577,6.4586 2.682797,9.15464 -3.736856,8.11995 4.063129,-0.32824 5.373423,5.49305 10.455693,5.28853 4.63456,2.20477 7.77237,10.67291 -0.21613,7.1478 -2.77074,-2.49821 -12.575734,-5.5801 -4.56731,-0.82823 7.39657,3.42523 13.27117,5.47432 20.40487,9.77384 5.10535,3.64464 7.31104,7.81908 9.24607,8.64541 -4.29084,2.04946 -12.93089,-1.63655 -6.51514,-2.76618 -4.00168,-0.72894 -8.50258,-2.75259 -4.66961,2.2333 3.25926,2.72127 11.54708,2.43298 13.0328,2.74132 -1.25934,2.77488 -3.4207,2.99556 0.0516,3.21078 -3.87375,2.06438 1.24216,2.38403 1.60114,3.56362 z m -7.9215,-22.36993 c -2.35682,-2.46475 -2.9662,-7.08134 -0.41852,-3.06426 1.30648,0.52466 4.18523,7.54428 0.41857,3.06426 l -5e-5,0 z m 25.79733,16.38693 c 1.47004,-0.0952 0.0427,1.11681 0,0 z m -29.51867,-22.43039 c -0.0904,-3.72637 0.8525,2.87419 0,0 z m -2.56392,-3.44965 c -2.96446,-5.72787 3.73721,1.62212 0,0 z M 89.382646,128.35916 c 1.7416,-0.46446 0.856841,2.97864 0,0 z m 24.728294,13.40357 c 1.06957,-4.01654 1.25692,3.37014 0,0 z M 96.64115,129.61525 c -1.231543,-2.21638 2.576009,2.07865 0,0 z m 14.99279,4.80618 c -2.80851,-6.29223 1.98836,-3.43699 0.62135,1.03124 l -0.62135,-1.03124 0,0 z M 85.778757,117.17864 c -1.255624,-2.06432 -3.332663,-8.12135 -2.663982,-9.97042 0.604935,3.0114 6.403914,12.95956 2.844571,4.12096 -3.933386,-7.40908 4.701805,2.40491 5.590052,4.2529 0.413624,1.83837 -2.426789,-0.50225 -0.502192,3.80828 -3.509809,-4.90766 -2.071967,2.71088 -5.268449,-2.21172 z m -7.990701,-5.50612 c 0.328938,-4.79981 1.829262,3.29132 0,0 z m 3.594293,1.23728 c 1.715175,-3.62282 2.908243,5.05052 0,0 z m -8.64616,-6.68847 c -2.974956,-2.95622 -5.127809,-5.68132 0.139193,-1.83474 2.029482,0.0792 -4.509002,-6.19705 0.488751,-1.99305 5.25531,0.95822 2.5951,8.61674 -0.627944,3.82779 z m 4.541717,-0.11873 c 1.727646,-1.71203 0.917172,1.6853 0,0 z m 2.794587,0.8959 c -2.619181,-4.9094 3.178801,2.05822 0,0 z m -5.55546,-5.30909 c -8.64844,-7.696511 10.867309,4.02451 1.4129,1.4269 l -1.412955,-1.42683 5.5e-5,-7e-5 z m 24.77908,14.39717 c -3.742506,-2.24398 -0.991777,-15.79747 0.284503,-6.52785 3.638294,-1.17695 -0.200879,4.78728 2.512784,4.73208 -0.42767,3.76305 -1.64169,5.11594 -2.797287,1.79577 z m 9.165207,5.41684 c 0.36705,-4.08462 0.77249,2.79262 0,0 z m -1.59198,-1.57295 c 0.41206,-1.74497 0.0426,2.05487 0,0 z M 76.213566,99.16032 c -5.556046,-7.665657 16.147323,7.75413 3.558556,1.9443 -1.315432,-0.34404 -2.898208,-0.46688 -3.558556,-1.9443 z m 17.649112,9.35749 c -0.525779,-6.45461 1.174169,1.06991 -1.92e-4,-2e-5 l 1.92e-4,2e-5 z m 13.399762,8.59585 c 1.03698,-3.67668 0.0773,2.43221 0,0 z M 77.064685,96.23472 c 3.302172,-0.706291 13.684695,5.79939 4.150224,1.85832 -1.059396,-1.17279 -3.317802,-0.63994 -4.150224,-1.85832 z m 28.356745,14.13312 c 0.35296,-6.60002 1.97138,-3.94233 0.0122,0.94474 l -0.0121,-0.94473 -5e-5,-1e-5 z M 79.52277,93.938099 c 1.345456,-1.97361 -3.571631,-8.923063 0.708795,-2.492797 1.849543,1.469605 5.355103,2.461959 2.260017,3.080216 4.867744,4.294162 -1.187244,1.163612 -2.968812,-0.587419 z m 24.49612,14.368161 c 0.92952,-7.51843 0.81971,4.40485 0,0 z M 76.712755,86.993902 c 1.027706,-0.439207 0.542746,1.369335 0,0 z m 6.389622,3.803092 c 1.644416,-3.450522 3.03351,3.848297 0,0 z m 18.023553,10.026276 c -0.0174,-1.3252 0.34003,1.92765 0,0 z m -1.04404,-2.31139 c -2.501612,-6.171646 2.32693,3.26759 0,0 z m -1.536003,-4.046372 c -0.419906,-2.550188 1.427129,3.203862 -7.3e-5,-9e-6 l 7.3e-5,9e-6 z m 2.499773,-4.063514 c -1.71663,-3.025123 2.16777,-13.331073 2.60122,-6.939418 -1.81185,4.980256 -0.52268,7.766309 0.74129,1.086388 2.33417,-5.257159 -0.50421,10.374054 -3.34255,5.853057 l 4e-5,-2.7e-5 z m 2.56889,-15.326649 c 0.74833,-0.918921 0.16609,1.107082 0,0 z m -4.290016,84.534235 c -1.017552,-0.88802 0.127775,0.56506 0,0 z m 8.837726,4.47065 c 4.91599,1.26135 4.89086,-0.76487 0.44782,-1.36683 -2.3898,-2.22316 -9.930475,-4.58124 -3.18119,-0.27586 0.44699,1.13227 1.85944,1.10589 2.73337,1.64269 z M 90.708067,152.48725 c 2.708244,2.01956 10.201213,5.72375 3.858186,0.76868 2.138588,-2.48467 -4.093336,-3.80722 -2.026067,-5.46927 -5.258175,-3.21755 -4.147962,-2.93133 -0.464111,-2.8301 -6.319385,-2.82462 0.912163,-2.61333 0.571661,-4.06067 -2.436706,-0.48126 -12.103074,-4.29664 -6.416395,0.31341 -5.780887,-2.94751 -1.377603,1.09799 -3.12488,0.67029 -5.911336,-1.61178 5.264392,4.50224 -0.938845,2.98448 3.391327,2.6875 9.128301,6.88393 1.433786,2.84407 -1.013816,1.45934 5.506273,3.67136 7.106665,4.77911 z m 9.243194,5.31013 c 11.238769,3.62163 -5.510018,-4.4246 0,0 z m 47.316399,28.66432 c 0.14496,-2.22965 -1.53604,1.90201 0,0 z m 4.86324,2.04679 c 2.59297,-2.51255 0.106,4.00222 4.29655,-0.61509 0.0453,-3.30544 -0.12904,-5.25783 -4.81563,-1.24252 -1.29194,0.71648 -1.86871,3.76288 0.51908,1.85761 z M 74.932378,140.02637 c -0.796355,-3.1304 -5.581949,-3.11418 0,0 z m 5.193029,3.40294 c -1.928397,-3.19739 -6.880525,-2.89469 0,0 z m 29.543373,17.81697 c 2.8844,2.56199 13.24761,1.87984 3.50331,0.31527 -1.44321,-2.13386 -9.16415,-1.6203 -3.50331,-0.31527 z m 40.61236,25.08153 c 4.43933,-3.72512 -4.30122,1.66183 0,0 z m 9.2328,6.34473 c 0.0277,-1.19543 -1.91352,0.52338 0,0 z m 0.0142,-1.6736 c 4.91602,-5.20866 -4.76346,0.30807 -4e-5,0 l 4e-5,0 z M 62.15981,129.33339 c -4.189944,-5.97826 -2.604586,-8.66544 -6.645136,-13.54677 -0.764913,-3.73279 -6.931672,-12.20326 -3.189579,-3.22947 3.42754,5.24836 4.446054,13.37434 9.834715,16.77624 z m 95.82635,60.00977 c 9.04429,-5.84575 -3.7125,-2.54641 0,0 z m 6.9041,2.70461 c 4.52911,-3.88867 -2.86491,-0.81334 0,0 z M 73.393094,133.41838 c 1.296204,-1.92838 -3.347642,-0.24666 0,0 z m 90.055596,56.78275 c 4.38526,-2.82746 -1.01036,-2.39335 -0.79483,0.26003 l 0.79484,-0.26003 -1e-5,0 z m -59.51386,-37.51178 c -0.15075,-1.90924 -2.31574,0.16206 0,0 z m 3.67794,2.11629 c -1.16888,-2.36318 -1.79716,0.37121 0,0 z m 62.8725,37.30625 c 5.61806,-4.05283 -3.4056,-0.77594 -1.17927,0.76785 l 1.17927,-0.76785 0,0 z m -2.15131,-1.03979 c 4.57663,-3.83506 -4.83183,1.69954 0,0 z m 10.99163,7.31983 c 3.0728,-2.05816 -3.73316,-0.66575 0,0 z M 76.211249,132.02781 c 4.118965,0.92286 16.460394,10.1439 9.179466,0.63772 -3.728991,-1.10384 -1.492605,-10.21906 -5.29621,-8.60579 2.552972,4.2649 2.100461,6.08018 -3.259642,3.3914 -6.736808,-3.28853 -3.785888,1.6297 -2.469293,2.98518 -1.794185,0.40772 2.373226,1.5572 1.845679,1.59149 z m -18.76588,-14.82026 c 0.737407,-3.04991 -6.789814,-16.77881 -3.554464,-6.87916 1.167861,2.07373 1.049123,6.00387 3.554464,6.87916 z m 34.443451,21.23513 c -2.120989,-1.77378 -0.100792,-0.25103 0,0 z m 5.222997,1.21548 c -0.0027,-3.23079 -5.77326,-1.31196 0,0 z m 45.261473,28.53321 c -0.86326,-2.20739 -3.41229,-0.0512 8e-5,4e-5 l -8e-5,-4e-5 z m 2.17351,1.58769 c -0.32087,-1.23546 -1.25399,0.23848 0,0 z m 17.94015,11.3001 c 1.72546,-1.27472 -2.15318,-0.1628 0,0 z M 66.819057,119.6006 c 4.935243,-1.91072 -5.28775,-1.36248 0,0 z m 71.569733,45.08937 c -0.0549,-3.19499 -3.14622,0.79264 0,0 z M 64.869152,115.05675 c 3.170167,-1.07084 -2.932663,-0.70531 0,0 z m 9.201532,4.45726 c -0.0575,-1.05014 -0.973336,0.39747 0,0 z m 112.231406,68.82181 c 4.0765,-0.8265 13.36606,2.07489 14.86752,-1.08086 -4.95044,-0.12019 -17.12734,-3.49263 -17.70346,0.80479 l 1.08368,0.17072 1.75226,0.10534 0,1e-5 z M 76.995161,120.25099 c 0.07087,-3.23755 -2.524669,-0.12092 0,0 z M 52.801998,103.4687 c -1.098703,-6.16843 -4.178791,-0.93357 0,0 z m 5.769195,1.45013 c 0.07087,-1.9807 -5.280562,-1.78224 0,0 z m 3.296917,1.61923 c -0.953019,-0.77196 -0.745959,0.97521 0,0 z m 20.744719,13.30775 c 0.976615,-0.89718 -2.312116,-0.66455 0,0 z M 59.672204,102.88617 c -0.557624,-4.65897 -6.681999,-0.69805 0,0 z M 47.844441,95.21166 c -0.168219,-2.150189 -1.152625,0.81111 0,0 z m 1.759336,-1.328672 c -0.28703,-2.549584 -1.510515,0.324387 0,0 z m 9.720792,5.802442 c 4.110486,-1.61465 -7.487254,-3.33984 -0.839893,-0.30506 l 0.839893,0.30506 z m 130.097601,80.35913 c 2.63092,-2.4121 -3.34373,-0.74577 0,0 z m 15.71669,8.14691 c 1.05433,-3.1186 -2.65452,0.41058 0,0 z M 60.318012,94.590436 c 0.433018,-3.016773 -3.258762,0.59902 0,0 z M 46.487687,85.324242 c -0.742965,-4.25911 -0.64134,-11.735065 6.465133,-9.208583 -9.485962,1.883339 6.56534,11.790095 4.538357,3.968363 3.988626,0.195294 7.802669,-2.357284 5.709487,1.516403 7.85876,-0.867958 13.307129,-7.682612 20.898169,-6.72768 5.913058,-0.782493 12.378182,-1.375955 18.750257,-3.756157 5.23905,-0.37743 10.28235,-6.018062 7.41068,-9.361383 -7.14456,-0.604513 -14.62339,0.289393 -22.520112,1.858993 -8.750559,1.819117 -16.699014,5.275307 -25.528125,6.758866 -8.605891,1.15604 1.730998,3.185165 -0.734074,3.637227 -4.490681,1.558136 5.355488,2.608852 -0.582182,4.251428 C 57.228283,77.56448 53.411411,76.304535 54.977788,72.440196 46.7341,73.50992 39.490264,76.931325 46.003276,85.320342 l 0.484402,0.0037 9e-6,-2.56e-4 z m 19.864291,-10.1168 c 1.932856,-7.120464 10.355229,5.859274 3.168052,0.945776 -0.858453,-0.642457 -2.2703,-1.166588 -3.168052,-0.945776 z m 0.376038,-3.452197 c 2.789661,-2.078257 1.482964,1.16516 0,0 z m 3.542213,0.05622 c 0.251833,-3.27648 8.108752,1.73455 1.295517,1.179499 l -1.295517,-1.179499 0,0 z m 4.84543,-1.948193 c 1.769481,-2.067535 0.50862,1.83906 0,0 z m 1.239563,-0.83005 c 2.946379,-3.540216 16.68561,-2.259413 6.628966,-0.34519 -2.695543,-2.029363 -4.761797,1.196575 -6.628966,0.34519 z m 17.930017,-2.763886 c -0.448199,-9.670222 8.907771,3.436477 0,0 z m 5.087539,-0.02784 c 1.860022,-4.873906 7.218072,-1.955774 0.860732,-0.979407 0.13805,0.518656 -0.18642,2.516173 -0.860732,0.979407 z M 58.311842,92.088739 c 5.55753,-3.403212 -5.899945,-2.952541 0,0 l 0,0 z m 4.109214,1.141866 c 1.948513,-2.071884 -4.233857,-0.840369 0,0 z M 50.313395,84.63767 c 3.175569,-2.439416 -3.757842,-0.927473 0,0 z M 214.41578,187.30012 c 0.0918,-2.83019 -2.42718,1.27537 0,0 z m -16.67487,-11.37935 c 0.47417,-3.25845 -2.14286,0.28408 0,0 z m 21.26022,12.47672 c 4.43994,0.015 13.45265,-1.37884 3.79217,-1.37442 -1.51594,0.23641 -8.83311,0.18571 -3.79216,1.37439 l -1e-5,3e-5 z M 66.035603,91.23339 c 3.593258,-0.246807 5.621861,-3.963629 -0.694932,-3.749977 -9.789949,-1.013541 8.637508,3.352129 -1.255898,2.10383 -1.329368,0.880346 1.873606,1.889721 1.95083,1.646147 z m 3.164618,1.601748 c -0.375177,-2.307063 -1.111156,1.225591 0,0 z m 3.753896,-10.009901 c 1.559281,-1.934055 -2.157697,-0.517053 0,0 z M 61.003998,62.84999 c 6.412879,-2.181631 15.182392,-4.633087 18.210335,1.074184 -3.081589,-3.70893 -1.24361,-7.360157 1.666959,-1.937407 4.115576,5.486669 6.175915,-2.495489 3.499086,-4.335821 3.050468,3.790246 6.520044,5.581281 2.042429,0.239564 4.865693,-5.852929 -9.742712,0.766433 -13.063105,0.699775 -1.597564,0.717062 -16.493576,3.79889 -12.355704,4.259705 z m 3.75831,-7.197834 c 3.657324,-2.760416 12.648968,1.641989 6.879078,-2.743367 -0.564117,-0.498292 -12.636077,3.325475 -6.879078,2.743367 z m 13.333489,0.550473 c 4.280389,0.109225 -1.84632,-5.750287 3.254304,-3.095159 -0.837696,-2.736627 -5.938558,-3.248956 -8.432316,-4.342312 -1.410474,2.502054 2.870977,7.471102 5.178012,7.437471 z M 67.100291,44.099162 c 1.480803,-2.007406 -2.59521,1.017699 0,0 z m 5.449586,1.304353 c 6.897867,-0.914901 -1.758292,-2.970542 -1.389954,-0.07352 l 1.389954,0.07352 0,-9e-6 z M 62.374386,37.441437 c -4.856866,-6.340205 9.133987,1.065769 4.199411,-5.572646 -4.153254,-3.307245 -8.144297,3.721775 -4.199411,5.572646 z m 62.330124,33.572802 c 2.22762,-3.948988 -9.19697,-5.323011 -1.5009,-1.399578 0.70858,0.236781 0.54821,1.6727 1.5009,1.399578 z" id="path2900" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -325,5 +165,126 @@ style="font-size:144px;font-family:High Tower Text;-inkscape-font-specification:High Tower Text" id="path2838" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/logo-full.png b/docs/_static/logo-full.png index f255eece8157a0ce6baf7e2e0b297c31ce153405..5deaf1b84eedf509fc1c0d853357b605880c8dfe 100644 GIT binary patch literal 20806 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 QOaK4?07*qoM6N<$f(R}^rT_o{ literal 23478 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 S^>upy0000=2.4' ], classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', From e0fa5aec3a13d9c3e8e27b53526fcee56ac0298d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 12 May 2010 01:30:10 +0200 Subject: [PATCH 0187/3747] Updated README as well. --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index e9eb1c7b..56d57f88 100644 --- a/README +++ b/README @@ -1,7 +1,7 @@ // Flask // - because sometimes a pocket knife is not enough + web development, one drop at a time ~ What is Flask? From 07581b2404924fe1833f596cfa09caf69868ad5b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 12 May 2010 01:32:29 +0200 Subject: [PATCH 0188/3747] HEAD is 0.5-dev --- CHANGES | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 5c645435..ee8dd002 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.5 +----------- + +Release date to be announced + Version 0.2 ----------- diff --git a/setup.py b/setup.py index 462cb6c7..ed922027 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ from setuptools import setup setup( name='Flask', - version='0.2', + version='0.5', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From f4269b1fe7fe4c17a99bc7ca0dddafd17bed14e9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 12 May 2010 02:06:40 +0200 Subject: [PATCH 0189/3747] Updated logo in the PDF docs --- docs/logo.pdf | Bin 28884 -> 23915 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/logo.pdf b/docs/logo.pdf index 49c5060e2563a6a47986c7a4694f04e859c31dc4..bd820c24728485ddc545e791da2a64d7794b6e6e 100644 GIT binary patch delta 23395 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 DL#nf| delta 28378 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 Date: Wed, 12 May 2010 02:34:22 +0200 Subject: [PATCH 0190/3747] Fixed typo --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 5af32ea8..dcf26b32 100644 --- a/flask.py +++ b/flask.py @@ -1113,7 +1113,7 @@ class Flask(_PackageBoundObject): with app.request_context(environ): do_something_with(request) - :params environ: a WSGI environment + :param environ: a WSGI environment """ return _RequestContext(self, environ) From 93a8ca02822d07cd9f13495f53d27ab22e9adf3a Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 12 May 2010 09:18:42 +0800 Subject: [PATCH 0191/3747] Fixed some minor typos throughout docs. --- docs/becomingbig.rst | 4 ++-- docs/design.rst | 6 +++--- docs/foreword.rst | 6 +++--- docs/installation.rst | 2 +- docs/quickstart.rst | 16 ++++++++-------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 02344720..fcffe7c2 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -25,7 +25,7 @@ In that case, it makes a lot of sense to use dotted names for the URL endpoints. Here are some suggestions for how Flask can be modified to better -accomodate large-scale applications: +accommodate large-scale applications: - implement dotted names for URL endpoints - get rid of the decorator function registering which causes a lot @@ -33,7 +33,7 @@ accomodate large-scale applications: also requires that the whole application is imported when the system initializes or certain URLs will not be available right away. A better solution would be to have one module with all URLs in there and - specifing the target functions explicitly or by name and importing + specifying the target functions explicitly or by name and importing them when needed. - switch to explicit request object passing. This requires more typing (because you now have something to pass around) but it makes it a diff --git a/docs/design.rst b/docs/design.rst index c4fd32dd..b2a56cf5 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -51,7 +51,7 @@ possible without hacks if the object were created ahead of time for you based on a class that is not exposed to you. But there is another very important reason why Flask depends on an -explicit instanciation of that class: the package name. Whenever you +explicit instantiation of that class: the package name. Whenever you create a Flask instance you usually pass it `__name__` as package name. Flask depends on that information to properly load resources relative to your module. With Python's outstanding support for reflection it can @@ -125,7 +125,7 @@ advantage. Flask is a framework that takes advantage of the work already done by Werkzeug to properly interface WSGI (which can be a complex task at times). Thanks to recent developments in the Python package -infrastructure, packages with depencencies are no longer an issue and +infrastructure, packages with dependencies are no longer an issue and there are very few reasons against having libraries that depend on others. @@ -140,7 +140,7 @@ isn't that a bad idea? Yes it is usually not such a bright idea to use thread locals. They cause troubles for servers that are not based on the concept of threads and make large applications harder to maintain. However Flask is just not designed -for large applications or asyncronous servers. Flask wants to make it +for large applications or asynchronous servers. Flask wants to make it quick and easy to write a traditional web application. Also see the :ref:`becomingbig` section of the documentation for some diff --git a/docs/foreword.rst b/docs/foreword.rst index deeff8ca..8abd4549 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -68,15 +68,15 @@ up in situations where we think "well, this is just far fetched, how could that possibly be exploited" and then an intelligent guy comes along and figures a way out to exploit that application. And don't think, your application is not important enough for hackers to take notice. Depending -ont he kind of attack, chances are there are automated botnets out there -trying to figure out how to fill your database with viagra adverisments. +on the kind of attack, chances are there are automated botnets out there +trying to figure out how to fill your database with viagra advertisements. So always keep that in mind when doing web development. Target Audience --------------- -Is Flask for you? Is your application small-ish (less than 4000 lines of +Is Flask for you? If your application small-ish (less than 4000 lines of Python code) and does not depend on too complex database structures, Flask is the Framework for you. It was designed from the ground up to be easy to use, based on established principles, good intentions and on top of two diff --git a/docs/installation.rst b/docs/installation.rst index f2f4905f..deedb12b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -122,7 +122,7 @@ Get the git checkout in a new virtualenv and run in develop mode:: ... Finished processing dependencies for Flask -This will pull in the depdenencies and activate the git head as current +This will pull in the dependencies and activate the git head as current version. Then you just have to ``git pull origin`` to get the latest version. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 59e36bcc..4fc0b68e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -83,7 +83,7 @@ you enable the debug support the server will reload itself on code changes and also provide you with a helpful debugger if things go wrong. There are two ways to enable debugging. Either set that flag on the -applciation object:: +application object:: app.debug = True app.run() @@ -139,7 +139,7 @@ likely he will like the page and come back next time. To add variable parts to a URL you can mark these special sections as ````. Such a part is then passed as keyword argument to -your function. Optionally a converter can be specifed by specifying a +your function. Optionally a converter can be specified by specifying a rule with ````. Here some nice examples:: @app.route('/user/') @@ -186,8 +186,8 @@ parameter. Here some examples: >>> with app.test_request_context(): ... print url_for('index') ... print url_for('login') -... print url_for('profile', username='John Doe') ... print url_for('login', next='/') +... print url_for('profile', username='John Doe') ... / /login @@ -319,7 +319,7 @@ Here's a simple example of how to render a template:: Flask will look for templates in the `templates` folder. So if your application is a module, that folder is next to that module, if it's a -pacakge it's actually inside your package: +package it's actually inside your package: **Case 1**: a module:: @@ -484,7 +484,7 @@ We recommend accessing URL parameters with `get` or by catching the `KeyError` because users might change the URL and presenting them a 400 bad request page in that case is a bit user unfriendly. -For a full list of methods and attribtues on that object, head over to the +For a full list of methods and attributes on that object, head over to the :class:`~flask.request` documentation. @@ -493,7 +493,7 @@ File Uploads Obviously you can handle uploaded files with Flask just as easy. Just make sure not to forget to set the ``enctype="multipart/form-data"`` -attribtue on your HTML form, otherwise the browser will not transmit your +attribute on your HTML form, otherwise the browser will not transmit your files at all. Uploaded files are stored in memory or at a temporary location on the @@ -576,7 +576,7 @@ you want to customize the error page, you can use the Note the ``404`` after the :func:`~flask.render_template` call. This tells Flask that the status code of that page should be 404 which means -not found. By default 200 is assumed which translats to: all went well. +not found. By default 200 is assumed which translates to: all went well. .. _sessions: @@ -586,7 +586,7 @@ Sessions Besides the request object there is also a second object called :class:`~flask.session` that allows you to store information specific to a user from one request to the next. This is implemented on top of cookies -for you and signes the cookies cryptographically. What this means is that +for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, unless he knows the secret key used for signing. From a5a41d40aaa9f6ed70b487133ddaf48ceb3d054f Mon Sep 17 00:00:00 2001 From: Zhao Xiaohong Date: Thu, 13 May 2010 14:42:03 +0800 Subject: [PATCH 0192/3747] Fix typo. --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index dcf26b32..6f8a21fc 100644 --- a/flask.py +++ b/flask.py @@ -85,7 +85,7 @@ class Response(ResponseBase): :meth:`~flask.Flask.make_response` will take care of that for you. If you want to replace the response object used you can subclass this and - set :attr:`~flask.Flask.request_class` to your subclass. + set :attr:`~flask.Flask.response_class` to your subclass. """ default_mimetype = 'text/html' From 693e4449f87950c764a928690f4437fd6b1b4d7d Mon Sep 17 00:00:00 2001 From: Zhao Xiaohong Date: Thu, 13 May 2010 15:22:03 +0800 Subject: [PATCH 0193/3747] Fix typo. --- flask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask.py b/flask.py index 6f8a21fc..084b6593 100644 --- a/flask.py +++ b/flask.py @@ -562,7 +562,7 @@ class Module(_PackageBoundObject): return f def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a modul. This + """Like :meth:`Flask.context_processor` but for a module. This function is only executed for requests handled by a module. """ self._record(lambda s: s.app.template_context_processors @@ -601,7 +601,7 @@ class Flask(_PackageBoundObject): app = Flask(__name__) """ - #: the class that is used for request objects. See :class:`~flask.request` + #: the class that is used for request objects. See :class:`~flask.Request` #: for more information. request_class = Request From 35ed617fe41b184288892f733ce07d94391b76b1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 00:37:55 +0200 Subject: [PATCH 0194/3747] Added support for flashing categories. This fixes #35. --- CHANGES | 2 ++ flask.py | 33 +++++++++++++++++++++++++++++---- tests/flask_tests.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index ee8dd002..3a7d696d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Version 0.5 Release date to be announced +- added support for categories for flashed messages. + Version 0.2 ----------- diff --git a/flask.py b/flask.py index 084b6593..d745027d 100644 --- a/flask.py +++ b/flask.py @@ -217,24 +217,49 @@ def get_template_attribute(template_name, attribute): attribute) -def flash(message): +def flash(message, category='message'): """Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. + .. versionchanged: 0.5 + `category` parameter added. + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string and be used as category. """ - session.setdefault('_flashes', []).append(message) + session.setdefault('_flashes', []).append((category, message)) -def get_flashed_messages(): +def get_flashed_messages(with_categories=False): """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return - the same messages. + the same messages. By default just the messages are returned, + but when `with_categories` is set to `True`, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Example usage: + + .. sourcecode:: html+jinja + + {% for category, msg in get_flashed_messages(with_categories=true) %} +

    {{ msg }} + {% endfor %} + + .. versionchanged:: 0.5 + `with_categories` parameter added. + + :param with_categories: set to `True` to also receive categories. """ flashes = _request_ctx_stack.top.flashes if flashes is None: _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) + if not with_categories: + return [x[1] for x in flashes] return flashes diff --git a/tests/flask_tests.py b/tests/flask_tests.py index c7cebb9c..f5dd12bb 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -157,6 +157,35 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert flask.session.modified assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] + def test_extended_flashing(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/') + def index(): + flask.flash(u'Hello World') + flask.flash(u'Hello World', 'error') + flask.flash(flask.Markup(u'Testing'), 'warning') + return '' + + @app.route('/test') + def test(): + messages = flask.get_flashed_messages(with_categories=True) + assert len(messages) == 3 + assert messages[0] == ('message', u'Hello World') + assert messages[1] == ('error', u'Hello World') + assert messages[2] == ('warning', flask.Markup(u'Testing')) + return '' + messages = flask.get_flashed_messages() + assert len(messages) == 3 + assert messages[0] == u'Hello World' + assert messages[1] == u'Hello World' + assert messages[2] == flask.Markup(u'Testing') + + c = app.test_client() + c.get('/') + c.get('/test') + def test_request_processing(self): app = flask.Flask(__name__) evts = [] From 931dcc4c80331d372d70d7a0a5c8e6d49223e2bf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 01:41:57 +0200 Subject: [PATCH 0195/3747] Fixed a doc bug and added distribtue docs. --- docs/installation.rst | 2 + docs/patterns/distribute.rst | 160 +++++++++++++++++++++++++++++++++++ docs/patterns/flashing.rst | 38 +++++++++ docs/patterns/index.rst | 1 + flask.py | 2 +- 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 docs/patterns/distribute.rst diff --git a/docs/installation.rst b/docs/installation.rst index deedb12b..6b4f955a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -19,6 +19,8 @@ lighttpd, on Google's App Engine or whatever you have in mind). So how do you get all that on your computer in no time? The most kick-ass method is virtualenv, so let's look at that first. +.. _virtualenv: + virtualenv ---------- diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst new file mode 100644 index 00000000..c4f6625d --- /dev/null +++ b/docs/patterns/distribute.rst @@ -0,0 +1,160 @@ +Deploying with Distribute +========================= + +`distribute`_, formerly setuptools, is an extension library that is +commonly used to (like the name says) distribute Python libraries and +extensions. It extends distutils, a basic module installation system +shipped with Python to also support various more complex constructs that +make larger applications easier to distribute: + +- **support for dependencies**: a library or application can declare a + list of other libraries it depends on which will be installed + automatically for you. +- **package registry**: setuptools registers your package with your + Python installation. This makes it possible to query information + provided by one package from another package. The best known feature of + this system is the entry point support which allows one package to + declare an "entry point" another package can hook into to extend the + other package. +- **installation manager**: `easy_install`, which comes with distribute + can install other libraries for you. You can also use `pip`_ which + sooner or later will replace `easy_install` which does more than just + installing packages for you. + +Flask itself, and all the libraries you can find on the cheeseshop will +are distributed with either distribute, the older setuptools or distutils. + +In this case we assume your application is called +`yourapplication.py` and you are not using a module, but a `package +`_. Distributing resources with standard modules is +not supported by `distribute`_ so we will not bother with it. If you have +not yet converted your application into a package, head over to the +:ref:`larger-applications` pattern to see how this can be done. + +Basic Setup Script +------------------ + +Because you have Flask running, you either have setuptools or distribute +available on your system anyways. If you do not, fear not, there is a +script to install it for you: `distribute_setup.py`_. Just download and +run with your Python interpreter. + +Standard disclaimer applies: :ref:`you better use a virtualenv +`. + +Your setup code always goes into a file named `setup.py` next to your +application. The name of the file is only convention, but because +everybody will look for a file with that name, you better not change it. + +Yes, even if you are using `distribute`, you are importing from a package +called `setuptools`. `distribute` is fully backwards compatible with +`setuptools`, so it also uses the same import name. + +A basic `setup.py` file for a Flask application looks like this:: + + from setuptools import setup + + setup( + name='Your Application', + version='1.0', + long_description=__doc__, + packages=['yourapplication'], + include_package_data=True, + zip_safe=False, + install_requires=['Flask'] + ) + +Please keep in mind that you have to list subpackages explicitly. If you +want distribute to lookup the packages for you automatically, you can use +the `find_packages` function:: + + from setuptools import setup, find_packages + + setup( + ... + packages=find_packages() + ) + +Most parameters to the `setup` function should be self explanatory, +`include_package_data` and `zip_safe` might not be. +`include_package_data` tells distribute to look for a `MANIFEST.in` file +and install all the entries that match as package data. We will use this +to distribute the static files and templates along with the Python module +(see :ref:`distributing-resources`). The `zip_safe` flag can be used to +force or prevent zip Archive creation. In general you probably don't want +your packages to be installed as zip files because some tools do not +support them and they make debugging a lot harder. + + +.. _distributing-resources: + +Distributing Resources +---------------------- + +If you try to install the package you just created, you will notice that +folders like `static` or `templates` are not installed for you. The +reason for this is that distribute does not know which files to add for +you. What you should do, is to create a `MANIFEST.in` file next to your +`setup.py` file. This file lists all the files that should be added to +your tarball:: + + recursive-include yourapplication/templates + recursive-include yourapplication/static + +Don't forget that even if you enlist them in your `MANIFEST.in` file, they +won't be installed for you unless you set the `include_package_data` +parameter of the `setup` function to `True`! + + +Declaring Dependencies +---------------------- + +Dependencies are declared in the `install_requires` parameter as list. +Each item in that list is the name of a package that should be pulled from +PyPI on installation. By default it will always use the most recent +version, but you can also provide minimum and maximum version +requirements. Here some examples:: + + install_requires=[ + 'Flask>=0.2', + 'SQLAlchemy>=0.6', + 'BrokenPackage>=0.7,<=1.0' + ] + +I mentioned earlier that dependencies are pulled from PyPI. What if you +want to depend on a package that cannot be found on PyPI and won't be +because it is an internal package you don't want to share with anyone? +Just still do as if there was a PyPI entry for it and provide a list of +alternative locations where distribute should look for tarballs:: + + dependency_links=['http://example.com/yourfiles'] + +Make sure that page has a directory listing and the links on the page are +pointing to the actual tarballs with their correct filenames as this is +how distribute will find the files. If you have an internal company +server that contains the packages, provide the URL to that server there. + + +Installing / Developing +----------------------- + +To install your application (ideally into a virtualenv) just run the +`setup.py` script with the `install` parameter. It will install your +application into the virtualenv's site-packages folder and also download +and install all dependencies:: + + $ python setup.py install + +If you are developing on the package and also want the requirements to be +installed, you can use the `develop` command instead:: + + $ python setup.py develop + +This has the advantage of just installing a link to the site-packages +folder instead of copying the data over. You can then continue to work on +the code without having to run `install` again after each change. + + +.. _distribute: http://pypi.python.org/pypi/distribute +.. _pip: http://pypi.python.org/pypi/pip +.. _distribute_setup.py: http://python-distribute.org/distribute_setup.py diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index fca9a9e1..16d8d371 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -11,6 +11,9 @@ possible to record a message at the end of a request and access it next request and only next request. This is usually combined with a layout template that does this. +Simple Flashing +--------------- + So here a full example:: from flask import flash, redirect, url_for, render_template @@ -79,3 +82,38 @@ And of course the login template:

    {% endblock %} + +Flashing With Categories +------------------------ + +.. versionadded:: 0.5 + +It is also possible to provide categories when flashing a message. The +default category if nothing is provided is ``'message'``. Alternative +categories can be used to give the user better feedback. For example +error messages could be displayed with a red background. + +To flash a message with a different category, just use the second argument +to the :func:`~flask.flash` function:: + + flash(u'Invalid password provided', 'error') + +Inside the template you then have to tell the +:func:`~flask.get_flashed_messages` function to also return the +categories. The loop looks slighty different in that situation then: + +.. sourcecode:: html+jinja + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +

      + {% for category, message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    + {% endif %} + {% endwith %} + +This is just one example of how to render these flashed messages. One +might also use the category to add a prefix such as +``Error:`` to the message. diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 9678e3be..ec3011f6 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -17,6 +17,7 @@ Snippet Archives `_. :maxdepth: 2 packages + distribute sqlite3 sqlalchemy fileuploads diff --git a/flask.py b/flask.py index d745027d..6fa3f7e3 100644 --- a/flask.py +++ b/flask.py @@ -230,7 +230,7 @@ def flash(message, category='message'): are recommended: ``'message'`` for any kind of message, ``'error'`` for errors, ``'info'`` for information messages and ``'warning'`` for warnings. However any - kind of string and be used as category. + kind of string can be used as category. """ session.setdefault('_flashes', []).append((category, message)) From b18560fcaa1739c81ff273951ebc6fa3c96fda2b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 01:51:16 +0200 Subject: [PATCH 0196/3747] s/will// --- docs/patterns/distribute.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index c4f6625d..76d4462a 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -21,7 +21,7 @@ make larger applications easier to distribute: sooner or later will replace `easy_install` which does more than just installing packages for you. -Flask itself, and all the libraries you can find on the cheeseshop will +Flask itself, and all the libraries you can find on the cheeseshop are distributed with either distribute, the older setuptools or distutils. In this case we assume your application is called From c6e6792cd52022add8455fc792b2ca73c62d3d65 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 02:01:23 +0200 Subject: [PATCH 0197/3747] Added MANIFEST.in. This fixes #39 --- MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..aee76e91 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include Makefile CHANGES LICENSE AUTHORS +recursive-include tests * +recursive-include docs * +prune docs/_build/doctrees From 8c26bec55c13804a01c11931263c9e9e4dab4afb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 12:10:45 +0200 Subject: [PATCH 0198/3747] Added links to downloadable docs in the docs --- docs/_templates/sidebarintro.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index b8381d7d..c281d3a9 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -5,6 +5,11 @@ not stable yet, but if you have some feedback,
    let me know.

    +

    Download Documentation

    +

    Useful Links

    • The Flask Website
    • From e7f67e13339073007a84271cce91200bfdb81006 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 16:06:30 +0200 Subject: [PATCH 0199/3747] Added logging support. --- CHANGES | 4 +++ flask.py | 74 ++++++++++++++++++++++++++++++++++++++------ tests/flask_tests.py | 64 +++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 3a7d696d..84aa4633 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,10 @@ Version 0.5 Release date to be announced - added support for categories for flashed messages. +- the application now configures a :class:`logging.Handler` and will + log request handling exceptions to that logger when not in debug + mode. This makes it possible to receive mails on server errors + for example. Version 0.2 ----------- diff --git a/flask.py b/flask.py index 6fa3f7e3..72c24029 100644 --- a/flask.py +++ b/flask.py @@ -21,7 +21,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ ImmutableDict, cached_property, wrap_file, Headers from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie # try to load the best simplejson implementation available. If JSON @@ -659,6 +659,18 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.2 use_x_sendfile = False + #: the logging format used for the debug logger. This is only used when + #: the application is in debug mode, otherwise the attached logging + #: handler does the formatting. + #: + #: .. versionadded:: 0.5 + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s, %(filename)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + #: options that are passed directly to the Jinja2 environment jinja_options = ImmutableDict( autoescape=True, @@ -753,6 +765,24 @@ class Flask(_PackageBoundObject): ) self.jinja_env.filters['tojson'] = _tojson_filter + @cached_property + def logger(self): + """A :class:`logging.Logger` object for this application. The + default configuration is to log to stderr if the application is + in debug mode. + """ + from logging import getLogger, StreamHandler, Formatter, DEBUG + class DebugHandler(StreamHandler): + def emit(x, record): + if self.debug: + StreamHandler.emit(x, record) + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(self.debug_log_format)) + logger = getLogger(self.import_name) + logger.addHandler(handler) + return logger + def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for the configured package is returned that looks up templates in the @@ -1010,6 +1040,38 @@ class Flask(_PackageBoundObject): self.template_context_processors[None].append(f) return f + def handle_http_exception(self, e): + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionadded: 0.5 + """ + handler = self.error_handlers.get(e.code) + if handler is None: + return e + return handler(e) + + def handle_exception(self, e): + """Default exception handling that kicks in when an exception + occours that is not catched. In debug mode the exception will + be re-raised immediately, otherwise it is logged an the handler + for an 500 internal server error is used. If no such handler + exists, a default 500 internal server error message is displayed. + + .. versionadded: 0.5 + """ + handler = self.error_handlers.get(500) + if self.debug: + raise + self.logger.exception('Exception on %s [%s]' % ( + request.path, + request.method + )) + if handler is None: + return InternalServerError() + return handler(e) + def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to @@ -1022,15 +1084,9 @@ class Flask(_PackageBoundObject): raise req.routing_exception return self.view_functions[req.endpoint](**req.view_args) except HTTPException, e: - handler = self.error_handlers.get(e.code) - if handler is None: - return e - return handler(e) + return self.handle_http_exception(e) except Exception, e: - handler = self.error_handlers.get(500) - if self.debug or handler is None: - raise - return handler(e) + return self.handle_exception(e) def make_response(self, rv): """Converts the return value from a view function to a real diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f5dd12bb..6cde2a3c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -16,6 +16,7 @@ import sys import flask import unittest import tempfile +from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from cStringIO import StringIO @@ -26,6 +27,16 @@ sys.path.append(os.path.join(example_path, 'flaskr')) sys.path.append(os.path.join(example_path, 'minitwit')) +@contextmanager +def catch_stderr(): + old_stderr = sys.stderr + sys.stderr = rv = StringIO() + try: + yield rv + finally: + sys.stderr = old_stderr + + class ContextTestCase(unittest.TestCase): def test_context_binding(self): @@ -585,6 +596,56 @@ class SendfileTestCase(unittest.TestCase): assert options['filename'] == 'index.txt' +class LoggingTestCase(unittest.TestCase): + + def test_debug_log(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + app.logger.warning('the standard library is dead') + return '' + + @app.route('/exc') + def exc(): + 1/0 + c = app.test_client() + + with catch_stderr() as err: + rv = c.get('/') + out = err.getvalue() + assert 'WARNING in flask_tests, flask_tests.py' in out + assert 'the standard library is dead' in out + + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + assert False, 'debug log ate the exception' + + def test_exception_logging(self): + from logging import StreamHandler + out = StringIO() + app = flask.Flask(__name__) + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + + err = out.getvalue() + assert 'Exception on / [GET]' in err + assert 'Traceback (most recent call last):' in err + assert '1/0' in err + assert 'ZeroDivisionError:' in err + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -592,8 +653,9 @@ def suite(): suite.addTest(unittest.makeSuite(ContextTestCase)) suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) suite.addTest(unittest.makeSuite(TemplatingTestCase)) - suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(ModuleTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) + suite.addTest(unittest.makeSuite(LoggingTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From ea5e654e9e19aab5089f9a71e679bcca11581e86 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 22:46:35 +0200 Subject: [PATCH 0200/3747] Added a documentation chapter about logging --- docs/_templates/sidebarintro.html | 5 +- docs/_themes | 2 +- docs/conf.py | 2 + docs/contents.rst.inc | 1 + docs/errorhandling.rst | 232 ++++++++++++++++++++++++++++++ docs/patterns/fileuploads.rst | 2 + docs/quickstart.rst | 28 ++++ flask.py | 24 ++-- tests/flask_tests.py | 22 +++ 9 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 docs/errorhandling.rst diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index c281d3a9..f2aecc74 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -5,7 +5,10 @@ not stable yet, but if you have some feedback, let me know.

      -

      Download Documentation

      +

      Other Formats

      +

      + You can download the documentation in other formats as well: +

      • as PDF
      • as zipped HTML diff --git a/docs/_themes b/docs/_themes index 11cb6b51..991997d6 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 11cb6b51c9ea3bc8f94afa3d7411b617f9db2570 +Subproject commit 991997d6d63a0cdcf7f4557a2dae5afa9b38b904 diff --git a/docs/conf.py b/docs/conf.py index 2048ab6e..b4731fff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,6 +50,8 @@ import pkg_resources # built documents. release = __import__('pkg_resources').get_distribution('Flask').version version = '.'.join(release.split('.')[:2]) +if 'dev' in version: + version = version.split('dev')[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 1e9e7df9..2e341107 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -13,6 +13,7 @@ web development. quickstart tutorial/index testing + errorhandling patterns/index deploying/index becomingbig diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst new file mode 100644 index 00000000..c83acbc7 --- /dev/null +++ b/docs/errorhandling.rst @@ -0,0 +1,232 @@ +Handling Application Errors +=========================== + +.. versionadded:: 0.5 + +Applications fail, server fail. Sooner or later you will see an exception +in production. Even if your code is 100% correct, you will still see +exceptions from time to time. Why? Because everything else involved will +fail. Here some situations where perfectly fine code can lead to server +errors: + +- the client terminated the request early and the application was still + reading from the incoming data. +- the database server was overloaded and could not handle the query. +- a filesystem is full +- a harddrive crashed +- a backend server overloaded +- a programming error in a library you are using +- network connection of the server to another system failed. + +And that's just a small sample of issues you could be facing. So how to +deal with that sort of problem? By default if your application runs in +production mode, Flask will display a very simple page for you and log the +exception to the :attr:`~flask.Flask.logger`. + +But there is more you can do, and we will cover some better setups to deal +with errors. + +Error Mails +----------- + +If the application runs in production mode (which it will do on your +server) you won't see any log messages by default. Why that? Flask tries +to be a zero-configuration framework and where should it drop the logs for +you if there is no configuration. Guessing is not a good idea because +changes are, the place it guessed is not the place where the user has the +permission to create a logfile. Also, for most small applications nobody +will look at the logs anyways. + +In fact, I promise you right now that if you configure a logfile for the +application errors you will never look at it except for debugging an issue +when a user reported it for you. What you want instead is a mail the +second the exception happened. Then you get an alert and you can do +something about it. + +Flask is using the Python builtin logging system and that one can actually +send you mails for errors which is probably what you want. Here is how +you can configure the Flask logger to send you mails for exceptions:: + + ADMINS = ['yourname@example.com'] + if not app.debug: + import logging + from logging.handlers import SMTPHandler + mail_handler = SMTPHandler('127.0.0.1', + 'server-error@example.com', + ADMINS, 'YourApplication Failed') + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + +So what just happened? We created a new +:class:`~logging.handlers.SMTPHandler` that will send mails with the mail +server listening on ``127.0.0.1`` to all the `ADMINS` from the address +*server-error@example.com* with the subject "YourApplication Failed". If +your mail server requires credentials these can also provided, for that +check out the documentation for the :class:`~logging.handlers.SMTPHandler`. + +We also tell the handler to only send errors and more critical messages. +Because we certainly don't want to get a mail for warnings or other +useless logs that might happen during request handling. + +Before you run that in production, please also look at :ref:`log-format` +to put more information into that error mail. That will save you from a +lot of frustration. + + +Logging to a File +----------------- + +Even if you get mails, you probably also want to log warnings. It's a +good idea to keep as much information around that might be required to +debug a problem. Please note that Flask itself will not issue any +warnings in the core system, so it's your responsibility to warn in the +code if something seems odd. + +There are a couple of handlers provided by the logging system out of the +box but not all of them are useful for basic error logging. The most +interesting are probably the following: + +- :class:`~logging.handlers.FileHandler` - logs messages to a file on the + filesystem. +- :class:`~logging.handlers.RotatingFileHandler` - logs messages to a file + on the filesystem and will rotate after a certain number of messages. +- :class:`~logging.handlers.NTEventLogHandler` - will log to the system + event log of a Windows system. If you are deploying on a Windows box, + this is what you want to use. +- :class:`~logging.handlers.SysLogHandler` - sends logs to a UNIX + syslog. + +Once you picked your log handler, do like you did with the SMTP handler +above, just make sure to use a lower setting (I would recommend +`WARNING`):: + + if not app.debug: + import logging + from logging.handlers import TheHandlerYouWant + file_handler = TheHandlerYouWant(...) + file_handler.setLevel(logging.WARNING) + app.logger.addHandler(file_handler) + +.. _log-format: + +Controlling the Log Format +-------------------------- + +By default a handler will only write the message string into a file or +send you that message as mail. But a log record stores more information +and it makes a lot of sense to configure your logger to also contain that +information so that you have a better idea of why that error happened, and +more importantly, where it did. + +A formatter can be instanciated with a format string. Note that +tracebacks are appended to the log entry automatically. You don't have to +do that in the log formatter format string. + +Here some example setups: + +Email +````` + +:: + + from logging import Formatter + mail_handler.setFormatter(Formatter(''' + Message type: %(levelname)s + Location: %(pathname)s:%(lineno)d + Module: %(module)s + Function: %(funcName)s + Time: %(asctime)s + + Message: + + %(message)s + ''')) + +File logging +```````````` + +:: + + from logging import Formatter + file_handler.setFormatter(Formatter( + '%(astime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]' + )) + + +Complex Log Formatting +`````````````````````` + +Here is a list of useful formatting variables for the format string. Note +that this list is not complete, consult the official documentation of the +:mod:`logging` package for a full list. + ++------------------+----------------------------------------------------+ +| Format | Description | ++==================+====================================================+ +| ``%(levelname)s``| Text logging level for the message | +| | (``'DEBUG'``, ``'INFO'``, ``'WARNING'``, | +| | ``'ERROR'``, ``'CRITICAL'``). | ++------------------+----------------------------------------------------+ +| ``%(pathname)s`` | Full pathname of the source file where the | +| | logging call was issued (if available). | ++------------------+----------------------------------------------------+ +| ``%(filename)s`` | Filename portion of pathname. | ++------------------+----------------------------------------------------+ +| ``%(module)s`` | Module (name portion of filename). | ++------------------+----------------------------------------------------+ +| ``%(funcName)s`` | Name of function containing the logging call. | ++------------------+----------------------------------------------------+ +| ``%(lineno)d`` | Source line number where the logging call was | +| | issued (if available). | ++------------------+----------------------------------------------------+ +| ``%(asctime)s`` | Human-readable time when the LogRecord` was | +| | created. By default this is of the form | +| | ``"2003-07-08 16:49:45,896"`` (the numbers after | +| | the comma are millisecond portion of the time). | +| | This can be changed by subclassing the formatter | +| | and overriding the | +| | :meth:`~logging.Formatter.formatTime` method. | ++------------------+----------------------------------------------------+ +| ``%(message)s`` | The logged message, computed as ``msg % args`` | ++------------------+----------------------------------------------------+ + +If you want to further customize the formatting, you can subclass the +formatter. The formatter has three interesting methods: + +:meth:`~logging.Formatter.format`: + handles the actual formatting. It is passed a + :class:`~logging.LogRecord` object and has to return the formatted + string. +:meth:`~logging.Formatter.formatTime`: + called for `asctime` formatting. If you want a different time format + you can override this method. +:meth:`~logging.Formatter.formatException` + called for exception formatting. It is passed a :attr:`~sys.exc_info` + tuple and has to return a string. The default is usually fine, you + don't have to override it. + +For more information, head over to the official documentation. + + +Other Libraries +--------------- + +So far we only configured the logger your application created itself. +Other libraries might log themselves as well. For example, SQLAlchemy use +logging heavily in the core. While there is a method to configure all +loggers at once in the :mod:`logging` package, I would not recommend using +it. There might be a situation in which you want to have multiple +separate applications running side by side in the same Python interpreter +and then it becomes impossible to have different logging setups for those. + +Instead, I would recommend figuring out which loggers you are interested +in, getting the loggers with the :func:`~logging.getLogger` function and +iterating over them to attach handlers:: + + from logging import getLogger + loggers = [app.logger, getLogger('sqlalchemy'), + getLogger('otherlibrary')] + for logger in loggers: + logger.addHandler(mail_handler) + logger.addHandler(file_handler) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index fd94605d..6d0bd0a1 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -1,3 +1,5 @@ +.. _uploading-files: + Uploading Files =============== diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4fc0b68e..745dedb9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -532,6 +532,8 @@ Werkzeug provides for you:: f.save('/var/www/uploads/' + secure_filename(f.filename)) ... +For some better examples, checkout the :ref:`uploading-files` pattern. + Cookies ``````` @@ -639,3 +641,29 @@ To flash a message use the :func:`~flask.flash` method, to get hold of the messages you can use :func:`~flask.get_flashed_messages` which is also available in the templates. Check out the :ref:`message-flashing-pattern` for a full example. + +Logging +------- + +.. versionadded:: 0.5 + +Sometimes you might be in the situation where you deal with data that +should be correct, but actually is not. For example you have some client +side code that sends an HTTP request to the server, and it's obviously +malformed. This might be caused by a user tempering with the data, or the +client code failed. Most the time, it's okay to reply with ``400 Bad +Request`` in that situation, but other times it is not and the code has to +continue working. + +Yet you want to log that something fishy happened. This is where loggers +come in handy. As of Flask 0.5 a logger is preconfigured for you to use. + +Here are some example log calls:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') + +The attached :attr:`~flask.Flask.logger` is a standard logging +:class:`~logging.Logger`, so head over to the official stdlib +documentation for more information. diff --git a/flask.py b/flask.py index 72c24029..9a919f7f 100644 --- a/flask.py +++ b/flask.py @@ -666,7 +666,7 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.5 debug_log_format = ( '-' * 80 + '\n' + - '%(levelname)s in %(module)s, %(filename)s:%(lineno)d]:\n' + + '%(levelname)s in %(module)s, %(pathname)s:%(lineno)d]:\n' + '%(message)s\n' + '-' * 80 ) @@ -769,7 +769,12 @@ class Flask(_PackageBoundObject): def logger(self): """A :class:`logging.Logger` object for this application. The default configuration is to log to stderr if the application is - in debug mode. + in debug mode. This logger can be used to (surprise) log messages. + Here some examples:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') """ from logging import getLogger, StreamHandler, Formatter, DEBUG class DebugHandler(StreamHandler): @@ -1085,8 +1090,6 @@ class Flask(_PackageBoundObject): return self.view_functions[req.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) - except Exception, e: - return self.handle_exception(e) def make_response(self, rv): """Converts the return value from a view function to a real @@ -1176,11 +1179,14 @@ class Flask(_PackageBoundObject): exception context to start the response """ with self.request_context(environ): - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) - response = self.process_response(response) + try: + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + response = self.process_response(response) + except Exception, e: + response = self.make_response(self.handle_exception(e)) return response(environ, start_response) def request_context(self, environ): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6cde2a3c..762b3913 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -645,6 +645,28 @@ class LoggingTestCase(unittest.TestCase): assert '1/0' in err assert 'ZeroDivisionError:' in err + def test_processor_exceptions(self): + app = flask.Flask(__name__) + @app.before_request + def before_request(): + if trigger == 'before': + 1/0 + @app.after_request + def after_request(response): + if trigger == 'after': + 1/0 + return response + @app.route('/') + def index(): + return 'Foo' + @app.errorhandler(500) + def internal_server_error(e): + return 'Hello Server Error', 500 + for trigger in 'before', 'after': + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert rv.data == 'Hello Server Error' + def suite(): from minitwit_tests import MiniTwitTestCase From 267f5f5c82a1ec9966bfb4e1e0c3f7cc4c80ed12 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 23:14:50 +0200 Subject: [PATCH 0201/3747] Added a pattern for custom error pages. --- docs/errorhandling.rst | 2 + docs/patterns/errorpages.rst | 77 ++++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 3 files changed, 80 insertions(+) create mode 100644 docs/patterns/errorpages.rst diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index c83acbc7..82ed3b6c 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -1,3 +1,5 @@ +.. _application-errors: + Handling Application Errors =========================== diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst new file mode 100644 index 00000000..5b4f6e80 --- /dev/null +++ b/docs/patterns/errorpages.rst @@ -0,0 +1,77 @@ +Custom Error Pages +================== + +Flask comes with a handy :func:`~flask.abort` function that aborts a +request with an HTTP error code early. It will also provide a plain black +and white error page for you with a basic description, but nothing fancy. + +Depening on the error code it is less or more likely for the user to +actually see such an error. + +Common Error Codes +------------------ + +The following error codes are some that are often displayed to the user, +even if the application behaves correctly: + +*404 Not Found* + The good old "chap, you made a mistake typing that URL" message. So + common that even novices to the internet know that 404 means: damn, + the thing I was looking for is not there. It's a very good idea to + make sure there is actually something useful on a 404 page, at least a + link back to the index. + +*403 Forbidden* + If you have some kind of access control on your website, you will have + to send a 403 code for disallowed resources. So make sure the user + is not lost when he tries to access a resource he cannot access. + +*410 Gone* + Did you know that there the "404 Not Found" has a brother named "410 + Gone"? Few people actually implement that, but the idea is that + resources that previously existed and got deleted answer with 410 + instead of 404. If you are not deleting documents permanently from + the database but just mark them as deleted, do the user a favour and + use the 410 code instead and display a message that what he was + looking for was deleted for all ethernity. + +*500 Internal Server Error* + Usually happens on programming errors or if the server is overloaded. + A terrible good idea to have a nice page there, because your + application *will* fail sooner or later (see also: + :ref:`application-errors`). + + +Error Handlers +-------------- + +An error handler is a function, just like a view function, but it is +called when an error happens and is passed that error. The error is most +likely a :exc:`~werkzeug.exceptions.HTTPException`, but in one case it +can be a different error: a handler for internal server errors will be +passed other exception instances as well if they are uncought. + +An error handler is registered with the :meth:`~flask.Flask.errorhandler` +decorator and the error code of the exception. Keep in mind that Flask +will *not* set the error code for you, so make sure to also provide the +HTTP status code when returning a response. + +Here an example implementation for a "404 Page Not Found" exception:: + + from flask import render_template + + @app.errorhandler(404) + def page_not_found(e): + return render_template('404.html'), 404 + +An example template might be this: + +.. sourcecode:: html+jinja + + {% extends "layout.html" %} + {% block title %}Page Not Found{% endblock %} + {% block body %} +

        Page Not Found

        +

        What you were looking for is just not there. +

        go somewhere nice + {% endblock %} diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index ec3011f6..61162372 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -27,3 +27,4 @@ Snippet Archives `_. templateinheritance flashing jquery + errorpages From 54c3df60cd533ea80d347f6715ee84f6a560b5d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 May 2010 23:21:22 +0200 Subject: [PATCH 0202/3747] Fixed a doc config error --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b4731fff..5127a66c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,9 +49,9 @@ import pkg_resources # |version| and |release|, also used in various other places throughout the # built documents. release = __import__('pkg_resources').get_distribution('Flask').version +if 'dev' in release: + release = release.split('dev')[0] + 'dev' version = '.'.join(release.split('.')[:2]) -if 'dev' in version: - version = version.split('dev')[0] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7bb0e33a687b3498c401a8dbcf7864d543a1817a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 01:31:51 +0200 Subject: [PATCH 0203/3747] Changed target audience section --- docs/foreword.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 8abd4549..34992a18 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -76,11 +76,13 @@ So always keep that in mind when doing web development. Target Audience --------------- -Is Flask for you? If your application small-ish (less than 4000 lines of -Python code) and does not depend on too complex database structures, Flask -is the Framework for you. It was designed from the ground up to be easy -to use, based on established principles, good intentions and on top of two -established libraries in widespread usage. +Is Flask for you? If your application small-ish and does not depend on +too complex database structures, Flask is the Framework for you. It was +designed from the ground up to be easy to use, based on established +principles, good intentions and on top of two established libraries in +widespread usage. Recent versions of Flask scale nicely within reasonable +bounds and if you grow larger, you won't have any troubles adjusting Flask +for your new application size. Flask serves two purposes: it's an example of how to create a minimal and opinionated framework on top of Werkzeug to show how this can be done, and From b26aeba972686b9cb9a9b111e1bc8997e1f259c8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 01:41:42 +0200 Subject: [PATCH 0204/3747] Fixed a testcase --- tests/flask_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 762b3913..14e3e384 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -614,7 +614,8 @@ class LoggingTestCase(unittest.TestCase): with catch_stderr() as err: rv = c.get('/') out = err.getvalue() - assert 'WARNING in flask_tests, flask_tests.py' in out + assert 'WARNING in flask_tests,' in out + assert 'flask_tests.py' in out assert 'the standard library is dead' in out with catch_stderr() as err: From e84140aba2786655bbddcedb363d16de1e285441 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 01:41:07 +0200 Subject: [PATCH 0205/3747] Started working on config support --- flask.py | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/flask.py b/flask.py index 9a919f7f..170b9abb 100644 --- a/flask.py +++ b/flask.py @@ -606,6 +606,21 @@ class Module(_PackageBoundObject): self._register_events.append(func) +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name): + self.__name__ = name + + def __get__(self, obj, type=None): + if obj is None: + return self + return obj.config[self.__name__] + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -639,25 +654,31 @@ class Flask(_PackageBoundObject): #: and the development server will no longer serve any static files. static_path = '/static' + #: the debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + debug = ConfigAttribute('debug') + #: if a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. - secret_key = None + secret_key = ConfigAttribute('secret_key') #: The secure cookie uses this for the name of the session cookie - session_cookie_name = 'session' + session_cookie_name = ConfigAttribute('session.cookie_name') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. - permanent_session_lifetime = timedelta(days=31) + permanent_session_lifetime = ConfigAttribute('session.permanent_lifetime') #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files #: sent with the :func:`send_file` method. #: #: .. versionadded:: 0.2 - use_x_sendfile = False + use_x_sendfile = ConfigAttribute('use_x_sendfile') #: the logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging @@ -677,15 +698,22 @@ class Flask(_PackageBoundObject): extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) - def __init__(self, import_name): + #: default configuration parameters + default_config = ImmutableDict({ + 'debug': False, + 'secret_key': None, + 'session.cookie_name': 'session', + 'session.permanent_lifetime': timedelta(days=31), + 'use_x_sendfile': False + }) + + def __init__(self, import_name, config=None): _PackageBoundObject.__init__(self, import_name) - #: the debug flag. Set this to `True` to enable debugging of - #: the application. In debug mode the debugger will kick in - #: when an unhandled exception ocurrs and the integrated server - #: will automatically reload the application if changes in the - #: code are detected. - self.debug = False + #: the configuration dictionary + self.config = self.default_config.copy() + if config: + self.config.update(config) #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and From c4cac0abc1a04710a25dcfc8d7f1b2de173546a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 02:36:50 +0200 Subject: [PATCH 0206/3747] Improved configuration support. --- docs/api.rst | 5 +++ flask.py | 105 ++++++++++++++++++++++++++++++++++++++----- tests/flask_tests.py | 24 ++++++++++ 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e13e9524..838b0bd5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -278,3 +278,8 @@ Template Rendering .. autofunction:: render_template_string .. autofunction:: get_template_attribute + +Configuration +------------- + +.. autoclass:: Config diff --git a/flask.py b/flask.py index 170b9abb..612e0b1f 100644 --- a/flask.py +++ b/flask.py @@ -19,7 +19,8 @@ from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers + ImmutableDict, cached_property, wrap_file, Headers, \ + import_string from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie @@ -621,6 +622,90 @@ class ConfigAttribute(object): obj.config[self.__name__] = value +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_module` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_module(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. The actual keys in the + config are however lowercased so they are converted for you. This makes + it possible to use lowercase values in the config file for temporary + values that are not added to the config or to define the config keys in + the same file that implements the application. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_pyfile(self, filename): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_module` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + """ + filename = os.path.join(self.root_path, filename) + d = type(sys)('config') + d.__file__ = filename + execfile(filename, d.__dict__) + self.from_module(d) + + def from_module(self, module): + """Updates the values from the given module. A module can be of one + of the following two types: + + - a string: in this case the module with that name will be imported + - an actual module reference: that module is used directly + + Just the uppercase variables in that module are stored in the config + after lowercasing. Example usage:: + + app.config.from_module('yourapplication.default_config') + from yourapplication import default_config + app.config.from_module(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth;`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param module: an import name or module + """ + if isinstance(module, basestring): + d = import_string(module).__dict__ + else: + d = module.__dict__ + for key, value in d.iteritems(): + if key.isupper(): + self[key.lower()] = value + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -666,12 +751,12 @@ class Flask(_PackageBoundObject): secret_key = ConfigAttribute('secret_key') #: The secure cookie uses this for the name of the session cookie - session_cookie_name = ConfigAttribute('session.cookie_name') + session_cookie_name = ConfigAttribute('session_cookie_name') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. - permanent_session_lifetime = ConfigAttribute('session.permanent_lifetime') + permanent_session_lifetime = ConfigAttribute('permanent_session_lifetime') #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files @@ -702,18 +787,18 @@ class Flask(_PackageBoundObject): default_config = ImmutableDict({ 'debug': False, 'secret_key': None, - 'session.cookie_name': 'session', - 'session.permanent_lifetime': timedelta(days=31), + 'session_cookie_name': 'session', + 'permanent_session_lifetime': timedelta(days=31), 'use_x_sendfile': False }) - def __init__(self, import_name, config=None): + def __init__(self, import_name): _PackageBoundObject.__init__(self, import_name) - #: the configuration dictionary - self.config = self.default_config.copy() - if config: - self.config.update(config) + #: the configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = Config(self.root_path, self.default_config) #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 14e3e384..33353c5b 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -27,6 +27,11 @@ sys.path.append(os.path.join(example_path, 'flaskr')) sys.path.append(os.path.join(example_path, 'minitwit')) +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + @contextmanager def catch_stderr(): old_stderr = sys.stderr @@ -669,6 +674,24 @@ class LoggingTestCase(unittest.TestCase): assert rv.data == 'Hello Server Error' +class ConfigTestCase(unittest.TestCase): + + def common_module_test(self, app): + assert app.secret_key == 'devkey' + assert app.config['test_key'] == 'foo' + assert 'ConfigTestCase' not in app.config + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile('flask_tests.py') + self.common_module_test(app) + + def test_config_from_module(self): + app = flask.Flask(__name__) + app.config.from_module(__name__) + self.common_module_test(app) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -679,6 +702,7 @@ def suite(): suite.addTest(unittest.makeSuite(ModuleTestCase)) suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) + suite.addTest(unittest.makeSuite(ConfigTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From 395ff8120ccf055614d03db7d2360ab1182c7c10 Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Tue, 18 May 2010 16:57:26 +0800 Subject: [PATCH 0207/3747] Fix typo. --- docs/patterns/distribute.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index 76d4462a..89a7e864 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -98,8 +98,8 @@ you. What you should do, is to create a `MANIFEST.in` file next to your `setup.py` file. This file lists all the files that should be added to your tarball:: - recursive-include yourapplication/templates - recursive-include yourapplication/static + recursive-include yourapplication/templates * + recursive-include yourapplication/static * Don't forget that even if you enlist them in your `MANIFEST.in` file, they won't be installed for you unless you set the `include_package_data` From 854e0e26d16408760072308c44b9160a9fb39abd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 18:05:05 +0200 Subject: [PATCH 0208/3747] Fixed some documentation issues. This fixes #41. --- docs/api.rst | 6 ++++++ docs/patterns/distribute.rst | 4 ++-- docs/patterns/fileuploads.rst | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e13e9524..2225ee07 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -210,6 +210,12 @@ thing, like it does for :class:`request` and :class:`session`. Useful Functions and Classes ---------------------------- +.. data:: current_app + + Points to the application handling the request. This is useful for + extensions that want to support multiple applications running side + by side. + .. autofunction:: url_for .. function:: abort(code) diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index 89a7e864..2826550c 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -25,8 +25,8 @@ Flask itself, and all the libraries you can find on the cheeseshop are distributed with either distribute, the older setuptools or distutils. In this case we assume your application is called -`yourapplication.py` and you are not using a module, but a `package -`_. Distributing resources with standard modules is +`yourapplication.py` and you are not using a module, but a :ref:`package +`. Distributing resources with standard modules is not supported by `distribute`_ so we will not bother with it. If you have not yet converted your application into a package, head over to the :ref:`larger-applications` pattern to see how this can be done. diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 6d0bd0a1..1174ebfc 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -54,7 +54,7 @@ the file and redirects the user to the URL for the uploaded file:: return '.' in filename and \ filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS - @app.route('/') + @app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': file = request.files['file'] From a5421c29acb6bfbd095dbeb4c8180a61bd1d4dab Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 19 May 2010 10:13:31 +0800 Subject: [PATCH 0209/3747] Added documentation for Gunicorn --- docs/deploying/others.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 4e2f966c..1ec94be4 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -46,3 +46,18 @@ event loop:: .. _Gevent: http://www.gevent.org/ .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ + + +Gunicorn +-------- + +`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork +worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ +and `greenlet`_. Running a Flask application on this server is quite simple:: + + gunicorn myproject:app + +.. _Gunicorn: http://gunicorn.org/ +.. _eventlet: http://eventlet.net/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html + From cbcd15c3b55cfb05f1b27aa2343f909876f8d3d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 May 2010 08:38:28 +0200 Subject: [PATCH 0210/3747] Beefed up latex output. Unfortunately parts look ugly. --- docs/conf.py | 1 + docs/contents.rst.inc | 42 --------------------------------------- docs/errorhandling.rst | 2 ++ docs/flaskstyle.sty | 9 +++++++++ docs/index.rst | 45 +++++++++++++++++++++++++++++++++++++++++- docs/latexindex.rst | 36 ++++++++++++++++++++++++++++++++- flask.py | 7 +++++++ 7 files changed, 98 insertions(+), 44 deletions(-) delete mode 100644 docs/contents.rst.inc diff --git a/docs/conf.py b/docs/conf.py index 5127a66c..f9f7867e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -190,6 +190,7 @@ latex_elements = { 'pointsize': '12pt', 'preamble': r'\usepackage{flaskstyle}' } +latex_use_parts = True latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc deleted file mode 100644 index 2e341107..00000000 --- a/docs/contents.rst.inc +++ /dev/null @@ -1,42 +0,0 @@ -User's Guide ------------- - -This part of the documentation is written text and should give you an idea -how to work with Flask. It's a series of step-by-step instructions for -web development. - -.. toctree:: - :maxdepth: 2 - - foreword - installation - quickstart - tutorial/index - testing - errorhandling - patterns/index - deploying/index - becomingbig - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you: - -.. toctree:: - :maxdepth: 2 - - api - -Additional Notes ----------------- - -Design notes, legal information and changelog are here for the interested: - -.. toctree:: - :maxdepth: 2 - - design - license - changelog diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 82ed3b6c..dcc2a2e9 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -163,6 +163,8 @@ Here is a list of useful formatting variables for the format string. Note that this list is not complete, consult the official documentation of the :mod:`logging` package for a full list. +.. tabularcolumns:: |p{3cm}|p{12cm}| + +------------------+----------------------------------------------------+ | Format | Description | +==================+====================================================+ diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty index 71d9ccc7..1d870050 100644 --- a/docs/flaskstyle.sty +++ b/docs/flaskstyle.sty @@ -70,4 +70,13 @@ \ChNumVar{\raggedleft \bfseries\Large} \ChTitleVar{\raggedleft \rm\Huge} +% use inconsolata font \usepackage{inconsolata} + +% fix single quotes, for inconsolata. (does not work) +%%\usepackage{textcomp} +%%\begingroup +%% \catcode`'=\active +%% \g@addto@macro\@noligs{\let'\textsinglequote} +%% \endgroup +%%\endinput diff --git a/docs/index.rst b/docs/index.rst index 2c3159c7..f2270422 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,5 @@ +:orphan: + Welcome to Flask ================ @@ -25,4 +27,45 @@ following links: .. _Jinja2: http://jinja.pocoo.org/2/ .. _Werkzeug: http://werkzeug.pocoo.org/ -.. include:: contents.rst.inc +User's Guide +------------ + +This part of the documentation is written text and should give you an idea +how to work with Flask. It's a series of step-by-step instructions for +web development. + +.. toctree:: + :maxdepth: 2 + + foreword + installation + quickstart + tutorial/index + testing + errorhandling + patterns/index + deploying/index + becomingbig + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you: + +.. toctree:: + :maxdepth: 2 + + api + +Additional Notes +---------------- + +Design notes, legal information and changelog are here for the interested: + +.. toctree:: + :maxdepth: 2 + + design + license + changelog diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 0b611b45..a4aa0b4a 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -1,4 +1,38 @@ +:orphan: + Flask Documentation =================== -.. include:: contents.rst.inc +User's Guide +------------ + +.. toctree:: + :maxdepth: 3 + + foreword + installation + quickstart + tutorial/index + testing + errorhandling + patterns/index + deploying/index + becomingbig + +API Reference +------------- + +.. toctree:: + :maxdepth: 3 + + api + +Additional Notes +---------------- + +.. toctree:: + :maxdepth: 3 + + design + license + changelog diff --git a/flask.py b/flask.py index 9a919f7f..ea3fcaa0 100644 --- a/flask.py +++ b/flask.py @@ -1097,6 +1097,8 @@ class Flask(_PackageBoundObject): The following types are allowed for `rv`: + .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| + ======================= =========================================== :attr:`response_class` the object is returned unchanged :class:`str` a response object is created with the @@ -1222,3 +1224,8 @@ current_app = LocalProxy(lambda: _request_ctx_stack.top.app) request = LocalProxy(lambda: _request_ctx_stack.top.request) session = LocalProxy(lambda: _request_ctx_stack.top.session) g = LocalProxy(lambda: _request_ctx_stack.top.g) + + +# script interface to run a development server +if __name__ == '__main__': + sys.exit(main(sys.argv)) From aa654128cf72e5a16a480eb59fc1ad551d39f5d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 May 2010 09:03:08 +0200 Subject: [PATCH 0211/3747] Added part style --- docs/flaskstyle.sty | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty index 1d870050..6da88db2 100644 --- a/docs/flaskstyle.sty +++ b/docs/flaskstyle.sty @@ -70,6 +70,42 @@ \ChNumVar{\raggedleft \bfseries\Large} \ChTitleVar{\raggedleft \rm\Huge} +\renewcommand\thepart{\@Roman\c@part} +\renewcommand\part{% + \if@noskipsec \leavevmode \fi + \cleardoublepage + \vspace*{8cm}% + \@afterindentfalse + \secdef\@part\@spart} + +\def\@part[#1]#2{% + \ifnum \c@secnumdepth >\m@ne + \refstepcounter{part}% + \addcontentsline{toc}{part}{\thepart\hspace{1em}#1}% + \else + \addcontentsline{toc}{part}{#1}% + \fi + {\parindent \z@ \center + \interlinepenalty \@M + \normalfont + \ifnum \c@secnumdepth >\m@ne + \rm\Large \partname~\thepart + \par\nobreak + \fi + \MakeUppercase{\rm\Huge #2}% + \markboth{}{}\par}% + \nobreak + \vskip 3ex + \@afterheading} +\def\@spart#1{% + {\parindent \z@ \center + \interlinepenalty \@M + \normalfont + \huge \bfseries #1\par}% + \nobreak + \vskip 3ex + \@afterheading} + % use inconsolata font \usepackage{inconsolata} From cc8332e9d99c08b77614e5acd7bd0e6d08dc29b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 May 2010 15:08:35 +0200 Subject: [PATCH 0212/3747] Added troubleshooting infos. This fixes #44 --- docs/_themes | 2 +- docs/deploying/cgi.rst | 8 +++++ docs/deploying/fastcgi.rst | 12 +++++++- docs/deploying/mod_wsgi.rst | 61 +++++++++++++++++++++++++++++++++++-- docs/flaskstyle.sty | 2 +- 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/_themes b/docs/_themes index 991997d6..09eeca52 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 991997d6d63a0cdcf7f4557a2dae5afa9b38b904 +Subproject commit 09eeca526b2b5675cc29f45917f5d0f795395035 diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 15b5ff1d..1168249c 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -9,6 +9,14 @@ This is also the way you can use a Flask application on Google's `AppEngine`_, there however the execution does happen in a CGI-like environment. The application's performance is unaffected because of that. +.. admonition:: Watch Out + + Please make sure in advance that your ``app.run()`` call you might + have in your application file, is inside an ``if __name__ == + '__main__':`` or moved to a separate file. Just make sure it's not + called because this will always start a local WSGI server which we do + not want if we deploy that application to CGI / appengine. + .. _AppEngine: http://code.google.com/appengine/ Creating a `.cgi` file diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index b549ddfd..7949cca4 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -8,6 +8,14 @@ a FastCGI server first. The most popular one is `flup`_ which we will use for this guide. Make sure to have it installed. +.. admonition:: Watch Out + + Please make sure in advance that your ``app.run()`` call you might + have in your application file, is inside an ``if __name__ == + '__main__':`` or moved to a separate file. Just make sure it's not + called because this will always start a local WSGI server which we do + not want if we deploy that application to FastCGI. + Creating a `.fcgi` file ----------------------- @@ -35,7 +43,9 @@ It makes sense to have that in `/var/www/yourapplication` or something similar. Make sure to set the executable bit on that file so that the servers -can execute it:: +can execute it: + +.. sourcecode:: text # chmod +x /var/www/yourapplication/yourapplication.fcgi diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index a7bbc114..03a82e8d 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -3,6 +3,14 @@ mod_wsgi (Apache) If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. +.. admonition:: Watch Out + + Please make sure in advance that your ``app.run()`` call you might + have in your application file, is inside an ``if __name__ == + '__main__':`` or moved to a separate file. Just make sure it's not + called because this will always start a local WSGI server which we do + not want if we deploy that application to mod_wsgi. + .. _Apache: http://httpd.apache.org/ Installing `mod_wsgi` @@ -14,12 +22,16 @@ a package manager or compile it yourself. The mod_wsgi `installation instructions`_ cover source installations on UNIX systems. -If you are using ubuntu / debian you can apt-get it and activate it as follows:: +If you are using ubuntu / debian you can apt-get it and activate it as follows: + +.. sourcecode:: text # apt-get install libapache2-mod-wsgi On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using -pkg_add:: +pkg_add: + +.. sourcecode:: text # pkg_add -r mod_wsgi @@ -78,3 +90,48 @@ For more information consult the `mod_wsgi wiki`_. .. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide .. _virtual python: http://pypi.python.org/pypi/virtualenv .. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ + +.. _* + +Toubleshooting +-------------- + +If your application does not run, follow this guide to troubleshoot: + +**Problem:** Application does not run, errorlog shows SystemExit ignored + You have a ``app.run()`` call in your application file that is not + guarded by an ``if __name__ == '__main__':`` condition. Either remove + that :meth:`~flask.Flask.run` call from the file and move it into a + separate `run.py` file or put it into such an if block. + +**Problem:** application gives permission errors + Probably caused by your application running as the wrong user. Make + sure the folders the application needs access to have the proper + privileges set and the application runs as the correct user (``user`` + and ``group`` parameter to the `WSGIDaemonProcess` directive) + +**Problem:** application dies with an error on print + Keep in mind that mod_wsgi disallows doing anything with + :data:`sys.stdout` and :data:`sys.stderr`. You can disable this + protection from the config by setting the `WSGIRestrictStdout` to + ``off``: + + .. sourcecode:: apache + + WSGIRestrictStdout Off + + Alternatively you can also replace the standard out in the .wsgi file + with a different stream:: + + import sys + sys.stdout = sys.stderr + +**Problem:** accessing resources gives IO errors + Your application probably is a single .py file you symlinked into the + site-packages folder. Please be aware that this does not work, + instead you either have to put the folder into the pythonpath the file + is stored in, or convert your application into a package. + + The reason for this is that for non-installed Packages, the module + filename is used to locate the resources and for symlinks the wrong + filename is picked up. diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty index 6da88db2..a86c6682 100644 --- a/docs/flaskstyle.sty +++ b/docs/flaskstyle.sty @@ -95,7 +95,7 @@ \MakeUppercase{\rm\Huge #2}% \markboth{}{}\par}% \nobreak - \vskip 3ex + \vskip 8ex \@afterheading} \def\@spart#1{% {\parindent \z@ \center From 9d98ad5e079b88fd9da87e47d55a11e465ab6bb0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 May 2010 15:24:17 +0200 Subject: [PATCH 0213/3747] Documented redirect behaviour. Fixes #42 --- docs/quickstart.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 745dedb9..f0fbeb64 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -160,6 +160,39 @@ The following converters exist: `path` like the default but also accepts slashes =========== =========================================== +.. admonition:: Unique URLs / Redirection Behaviour + + Flask's URL rules are based on Werkzeug's routing module. The idea + behind that module is to ensure nice looking and also unique URLs based + on behaviour Apache and earlier servers coined. + + Take these two rules:: + + @app.route('/projects/') + def projects(): + pass + + @app.route('/about') + def about(): + pass + + They look rather similar, the difference is the trailing slash in the + URL *definition*. In the first case, the canonical URL for the + `projects` endpoint has a trailing slash. It's similar to a folder in + that sense. Accessing it without a trailing slash will cause Flask to + redirect to the canonical URL with the trailing slash. + + However in the second case the URL is defined without a slash so it + behaves similar to a file and accessing the URL with a trailing slash + will be a 404 error. + + Why is this? This allows relative URLs to continue working if users + access the page when they forget a trailing slash. This behaviour is + also consistent with how Apache and other servers work. Also, the URLs + will stay unique which helps search engines not indexing the same page + twice. + + .. _url-building: URL Building From ef0dc1800f7558abbefe070f361b97b9161b2452 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 May 2010 18:37:48 +0200 Subject: [PATCH 0214/3747] Added interactive Python docs, fixed part style. --- CHANGES | 4 ++ docs/contents.rst.inc | 43 +++++++++++++++++ docs/flaskstyle.sty | 8 +-- docs/index.rst | 43 +---------------- docs/latexindex.rst | 34 +------------ docs/shell.rst | 110 ++++++++++++++++++++++++++++++++++++++++++ flask.py | 37 +++++++++++++- tests/flask_tests.py | 17 +++++++ 8 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 docs/contents.rst.inc create mode 100644 docs/shell.rst diff --git a/CHANGES b/CHANGES index 84aa4633..47dd08b6 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Release date to be announced log request handling exceptions to that logger when not in debug mode. This makes it possible to receive mails on server errors for example. +- added support for context binding that does not require the use of + the with statement for playing in the console. +- the request context is now available within the with statement making + it possible to further push the request context or pop it. Version 0.2 ----------- diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc new file mode 100644 index 00000000..66dc0ead --- /dev/null +++ b/docs/contents.rst.inc @@ -0,0 +1,43 @@ +User's Guide +------------ + +This part of the documentation is written text and should give you an idea +how to work with Flask. It's a series of step-by-step instructions for +web development. + +.. toctree:: + :maxdepth: 2 + + foreword + installation + quickstart + tutorial/index + testing + errorhandling + shell + patterns/index + deploying/index + becomingbig + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + +Additional Notes +---------------- + +Design notes, legal information and changelog are here for the interested. + +.. toctree:: + :maxdepth: 2 + + design + license + changelog diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty index a86c6682..b2d073d0 100644 --- a/docs/flaskstyle.sty +++ b/docs/flaskstyle.sty @@ -1,4 +1,3 @@ -\pagenumbering{arabic} \definecolor{TitleColor}{rgb}{0,0,0} \definecolor{InnerLinkColor}{rgb}{0,0,0} @@ -72,9 +71,10 @@ \renewcommand\thepart{\@Roman\c@part} \renewcommand\part{% + \pagestyle{empty} \if@noskipsec \leavevmode \fi \cleardoublepage - \vspace*{8cm}% + \vspace*{6cm}% \@afterindentfalse \secdef\@part\@spart} @@ -85,7 +85,7 @@ \else \addcontentsline{toc}{part}{#1}% \fi - {\parindent \z@ \center + {\parindent \z@ %\center \interlinepenalty \@M \normalfont \ifnum \c@secnumdepth >\m@ne @@ -98,7 +98,7 @@ \vskip 8ex \@afterheading} \def\@spart#1{% - {\parindent \z@ \center + {\parindent \z@ %\center \interlinepenalty \@M \normalfont \huge \bfseries #1\par}% diff --git a/docs/index.rst b/docs/index.rst index f2270422..e8ea5c33 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,45 +27,4 @@ following links: .. _Jinja2: http://jinja.pocoo.org/2/ .. _Werkzeug: http://werkzeug.pocoo.org/ -User's Guide ------------- - -This part of the documentation is written text and should give you an idea -how to work with Flask. It's a series of step-by-step instructions for -web development. - -.. toctree:: - :maxdepth: 2 - - foreword - installation - quickstart - tutorial/index - testing - errorhandling - patterns/index - deploying/index - becomingbig - -API Reference -------------- - -If you are looking for information on a specific function, class or -method, this part of the documentation is for you: - -.. toctree:: - :maxdepth: 2 - - api - -Additional Notes ----------------- - -Design notes, legal information and changelog are here for the interested: - -.. toctree:: - :maxdepth: 2 - - design - license - changelog +.. include:: contents.rst.inc diff --git a/docs/latexindex.rst b/docs/latexindex.rst index a4aa0b4a..288197c3 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -3,36 +3,4 @@ Flask Documentation =================== -User's Guide ------------- - -.. toctree:: - :maxdepth: 3 - - foreword - installation - quickstart - tutorial/index - testing - errorhandling - patterns/index - deploying/index - becomingbig - -API Reference -------------- - -.. toctree:: - :maxdepth: 3 - - api - -Additional Notes ----------------- - -.. toctree:: - :maxdepth: 3 - - design - license - changelog +.. include:: contents.rst.inc diff --git a/docs/shell.rst b/docs/shell.rst new file mode 100644 index 00000000..65e44ce7 --- /dev/null +++ b/docs/shell.rst @@ -0,0 +1,110 @@ +Working with the Shell +====================== + +.. versionadded:: 0.5 + +One of the reasons everybody loves Python is the interactive shell. It +basically allows you to execute Python commands in real time and +immediately get results back. Flask itself does not come with an +interactive shell, because it does not require any specific setup upfront, +just import your application and start playing around. + +There are however some handy helpers to make playing around in the shell a +more pleasant experience. The main issue with interactive console +sessions is that you're not triggering a request like a browser does which +means that :data:`~flask.g`, :data:`~flask.request` and others are not +available. But the code you want to test might depend on them, so what +can you do? + +This is where some helper functions come in handy. Keep in mind however +that these functions are not only there for interactive shell usage, but +also for unittesting and other situations that require a faked request +context. + +Diving into Context Locals +-------------------------- + +Say you have a utility function that returns the URL the user should be +redirected to. Imagine it would always redirect to the URL's ``next`` +parameter or the HTTP referrer or the index page:: + + from flask import request, url_for + + def redirect_url(): + return request.args.get('next') or \ + request.referrer or \ + url_for('index') + +As you can see, it accesses the request object. If you try to run this +from a plain Python shell, this is the exception you will see: + +>>> redirect_url() +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'NoneType' object has no attribute 'request' + +That makes a lot of sense because we currently do not have a request we +could access. So we have to make a request and bind it to the current +context. The :attr:`~flask.Flask.test_request_context` method can create +us a request context: + +>>> ctx = app.test_request_context('/?next=http://example.com/') + +This context can be used in two ways. Either with the `with` statement +(which unfortunately is not very handy for shell sessions). The +alternative way is to call the `push` and `pop` methods: + +>>> ctx.push() + +From that point onwards you can work with the request object: + +>>> redirect_url() +u'http://example.com/' + +Until you call `pop`: + +>>> ctx.pop() +>>> redirect_url() +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'NoneType' object has no attribute 'request' + + +Firing Before/After Request +--------------------------- + +By just creating a request context, you still don't have run the code that +is normally run before a request. This probably results in your database +being unavailable, the current user not being stored on the +:data:`~flask.g` object etc. + +This however can easily be done yourself. Just call +:meth:`~flask.Flask.preprocess_request`: + +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> app.preprocess_request() + +Keep in mind that the :meth:`~flask.Flask.preprocess_request` function +might return a response object, in that case just ignore it. + +To shutdown a request, you need to trick a bit before the after request +functions (triggered by :meth:`~flask.Flask.process_response`) operate on +a response object: + +>>> app.process_response(app.response_class()) + +>>> ctx.pop() + + +Further Improving the Shell Experience +-------------------------------------- + +If you like the idea of experimenting in a shell, create yourself a module +with stuff you want to star import into your interactive session. There +you could also define some more helper methods for common things such as +initializing the database, dropping tables etc. + +Just put them into a module (like `shelltools` and import from there): + +>>> from shelltools import * diff --git a/flask.py b/flask.py index ea3fcaa0..d3391a43 100644 --- a/flask.py +++ b/flask.py @@ -147,15 +147,24 @@ class _RequestContext(object): except HTTPException, e: self.request.routing_exception = e - def __enter__(self): + def push(self): + """Binds the request context.""" _request_ctx_stack.push(self) + def pop(self): + """Pops the request context.""" + _request_ctx_stack.pop() + + def __enter__(self): + self.push() + return self + def __exit__(self, exc_type, exc_value, tb): # do not pop the request stack if we are in debug mode and an # exception happened. This will allow the debugger to still # access the request object in the interactive shell. if tb is None or not self.app.debug: - _request_ctx_stack.pop() + self.pop() def url_for(endpoint, **values): @@ -1202,6 +1211,30 @@ class Flask(_PackageBoundObject): with app.request_context(environ): do_something_with(request) + The object returned can also be used without the `with` statement + which is useful for working in the shell. The example above is + doing exactly the same as this code:: + + ctx = app.request_context(environ) + ctx.push() + try: + do_something_with(request) + finally: + ctx.pop() + + The big advantage of this approach is that you can use it without + the try/finally statement in a shell for interactive testing: + + >>> ctx = app.test_request_context() + >>> ctx.bind() + >>> request.path + u'/' + >>> ctx.unbind() + + .. versionchanged:: 0.5 + Added support for non-with statement usage and `with` statement + is now passed the ctx object. + :param environ: a WSGI environment """ return _RequestContext(self, environ) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 14e3e384..17f73ad7 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -53,6 +53,23 @@ class ContextTestCase(unittest.TestCase): with app.test_request_context('/meh'): assert meh() == 'http://localhost/meh' + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + assert index() == 'Hello World!' + ctx.pop() + try: + index() + except AttributeError: + pass + else: + assert 0, 'expected runtime error' + class BasicFunctionalityTestCase(unittest.TestCase): From 6c095deda51efb34cd18bd23411ebd6f2a0cdfe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 25 May 2010 08:17:55 +0200 Subject: [PATCH 0215/3747] Fixed docstring. This fixes #45 --- flask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask.py b/flask.py index d3391a43..cbb10b59 100644 --- a/flask.py +++ b/flask.py @@ -1004,14 +1004,14 @@ class Flask(_PackageBoundObject): error code. Example:: @app.errorhandler(404) - def page_not_found(): + def page_not_found(error): return 'This page does not exist', 404 You can also register a function as error handler without using the :meth:`errorhandler` decorator. The following example is equivalent to the one above:: - def page_not_found(): + def page_not_found(error): return 'This page does not exist', 404 app.error_handlers[404] = page_not_found From 133920c83bf78db17631b637b8b2ffabcb26f3d4 Mon Sep 17 00:00:00 2001 From: Marian Sigler Date: Tue, 25 May 2010 01:58:10 +0800 Subject: [PATCH 0216/3747] Fix typo in docs. --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 6b4f955a..31c40023 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -41,7 +41,7 @@ an installation but a clever way to keep things separated. So let's see how that works! If you are on OS X or Linux chances are that one of the following two -commands will for for you:: +commands will work for you:: $ sudo easy_install virtualenv From 7829e6ec98f9c84159dcb2aa9fb210b88d97dd33 Mon Sep 17 00:00:00 2001 From: Marian Sigler Date: Tue, 25 May 2010 05:07:09 +0800 Subject: [PATCH 0217/3747] quickstart docs: Use a real value for secret_key to better illustrate how a secret key should look. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f0fbeb64..cb887251 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -654,7 +654,7 @@ sessions work:: session.pop('username', None) # set the secret key. keep this really secret: - app.secret_key = 'the secret key' + app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' The here mentioned :func:`~flask.escape` does escaping for you if you are not using the template engine (like in this example). From 9d19b77acf413de77b39ed1c6d972fb1e5fef1c3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 26 May 2010 14:49:01 +0200 Subject: [PATCH 0218/3747] Added lazyloading pattern and explicit chapter links in tutorial. This fixes #49. --- docs/_themes | 2 +- docs/conf.py | 4 +- docs/patterns/index.rst | 1 + docs/patterns/lazyloading.rst | 104 +++++++++++++++++++++++++++++++++ docs/tutorial/css.rst | 4 ++ docs/tutorial/dbcon.rst | 4 ++ docs/tutorial/dbinit.rst | 4 ++ docs/tutorial/folders.rst | 4 ++ docs/tutorial/introduction.rst | 4 ++ docs/tutorial/schema.rst | 4 ++ docs/tutorial/setup.rst | 4 ++ docs/tutorial/templates.rst | 4 ++ docs/tutorial/testing.rst | 2 + docs/tutorial/views.rst | 4 ++ examples/minitwit/minitwit.py | 1 + 15 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 docs/patterns/lazyloading.rst diff --git a/docs/_themes b/docs/_themes index 09eeca52..91eee537 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 09eeca526b2b5675cc29f45917f5d0f795395035 +Subproject commit 91eee537e91594f752224a5847719f6d4fb38c2d diff --git a/docs/conf.py b/docs/conf.py index f9f7867e..18e8f309 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -140,7 +140,7 @@ html_sidebars = { #html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +html_use_modindex = False # If false, no index is generated. #html_use_index = True @@ -152,7 +152,7 @@ html_sidebars = { #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 61162372..fcd482d7 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -28,3 +28,4 @@ Snippet Archives `_. flashing jquery errorpages + lazyloading diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst new file mode 100644 index 00000000..2c331ca9 --- /dev/null +++ b/docs/patterns/lazyloading.rst @@ -0,0 +1,104 @@ +Lazily Loading Views +==================== + +Flask is usually used with the decorators. Decorators are simple and you +have the URL right next to the function that is called for that specific +URL. However there is a downside to this approach: it means all your code +that uses decorators has to be imported upfront or Flask will never +actually find your function. + +This can be a problem if your application has to import quick. It might +have to do that on systems like Google's AppEngine or other systems. So +if you suddenly notice that your application outgrows this approach you +can fall back to a centralized URL mapping. + +The system that enables having a central URL map is the +:meth:`~flask.Flask.add_url_rule` function. Instead of using decorators, +you have a file that sets up the application with all URLs. + +Converting to Centralized URL Map +--------------------------------- + +Imagine the current application looks somewhat like this:: + + from flask import Flask + app = Flask(__name__) + + @app.route('/') + def index(): + pass + + @app.route('/user/') + def user(username): + pass + +Then the centralized approach you would have one file with the views +(`views.py`) but without any decorator:: + + def index(): + pass + + def user(username): + pass + +And then a file that sets up an application which maps the functions to +URLs:: + + from flask import Flask + from yourapplication import views + app = Flask(__name__) + app.add_url_rule('/', view_func=views.index) + app.add_url_rule('/user/', view_func=views.user) + +Loading Late +------------ + +So far we only split up the views and the routing, but the module is still +loaded upfront. The trick to actually load the view function as needed. +This can be accomplished with a helper class that behaves just like a +function but internally imports the real function on first use:: + + from werkzeug import import_string, cached_property + + class LazyView(object): + + def __init__(self, import_name): + self.__module__, self.__name__ = import_name.rsplit('.', 1) + self.import_name = import_name + + @cached_property + def view(self): + return import_string(self.import_name) + + def __call__(self, *args, **kwargs): + return self.view(*args, **kwargs) + +What's important here is is that `__module__` and `__name__` are properly +set. This is used by Flask internally to figure out how to do name the +URL rules in case you don't provide a name for the rule yourself. + +Then you can define your central place to combine the views like this:: + + from flask import Flask + from yourapplication.helpers import LazyView + app = Flask(__name__) + app.add_url_rule('/', + view_func=LazyView('yourapplication.views.index')) + app.add_url_rule('/user/', + view_func=LazyView('yourapplication.views.user')) + +You can further optimize this in terms of amount of keystrokes needed to +write this by having a function that calls into +:meth:`~flask.Flask.add_url_rule` by prefixing a string with the project +name and a dot, and by wrapping `view_func` in a `LazyView` as needed:: + + def url(url_rule, import_name, **options): + view = LazyView('yourapplication.' + import_name) + app.add_url_rule(url_rule, view_func=view, **options) + + url('/', 'views.index') + url('/user/', 'views.user') + +One thing to keep in mind is that before and after request handlers have +to be in a file that is imported upfront to work propery on the first +request. The same goes for any kind of remaining decorator. diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst index c2a6ba5b..76265a65 100644 --- a/docs/tutorial/css.rst +++ b/docs/tutorial/css.rst @@ -1,3 +1,5 @@ +.. _tutorial-css: + Step 7: Adding Style ==================== @@ -25,3 +27,5 @@ folder we created before: .flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } .error { background: #F0D6D6; padding: 0.5em; } + +Continue with :ref:`tutorial-testing`. diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 9741dabb..50aba04d 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -1,3 +1,5 @@ +.. _tutorial-dbcon: + Step 4: Request Database Connections ------------------------------------ @@ -31,3 +33,5 @@ request only and is available from within each function. Never store such things on other objects because this would not work with threaded environments. That special :data:`~flask.g` object does some magic behind the scenes to ensure it does the right thing. + +Continue to :ref:`tutorial-views`. diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 0dc87d58..602b999d 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -1,3 +1,5 @@ +.. _tutorial-dbinit: + Step 3: Creating The Database ============================= @@ -61,3 +63,5 @@ importing and calling that function:: If you get an exception later that a table cannot be found check that you did call the `init_db` function and that your table names are correct (singular vs. plural for example). + +Continue with :ref:`tutorial-dbcon` diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst index 80697a94..5e685c7e 100644 --- a/docs/tutorial/folders.rst +++ b/docs/tutorial/folders.rst @@ -1,3 +1,5 @@ +.. _tutorial-folders: + Step 0: Creating The Folders ============================ @@ -16,4 +18,6 @@ This is the place where css and javascript files go. Inside the `templates` folder Flask will look for `Jinja2`_ templates. Drop all the templates there. +Continue with :ref:`tutorial-schema`. + .. _Jinja2: http://jinja.pocoo.org/2/ diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index 04396a9d..ff7cdcab 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -1,3 +1,5 @@ +.. _tutorial-introduction: + Introducing Flaskr ================== @@ -26,4 +28,6 @@ Here a screenshot from the final application: :class: screenshot :alt: screenshot of the final application +Continue with :ref:`tutorial-folders`. + .. _SQLAlchemy: http://www.sqlalchemy.org/ diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst index ed329539..c078667e 100644 --- a/docs/tutorial/schema.rst +++ b/docs/tutorial/schema.rst @@ -1,3 +1,5 @@ +.. _tutorial-schema: + Step 1: Database Schema ======================= @@ -19,3 +21,5 @@ This schema consists of a single table called `entries` and each row in this table has an `id`, a `title` and a `text`. The `id` is an automatically incrementing integer and a primary key, the other two are strings that must not be null. + +Continue with :ref:`tutorial-setup`. diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 1214c22f..f9a0b302 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -1,3 +1,5 @@ +.. _tutorial-setup: + Step 2: Application Setup Code ============================== @@ -62,3 +64,5 @@ focus on that a little later. First we should get the database working. Want your server to be publically available? Check out the :ref:`externally visible server ` section for more information. + +Continue with :ref:`tutorial-dbinit`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index 66b1dec6..5ec5584d 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -1,3 +1,5 @@ +.. _tutorial-templates: + Step 6: The Templates ===================== @@ -105,3 +107,5 @@ the user to login:

    {% endblock %} + +Continue with :ref:`tutorial-css`. diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index c3075e3a..051e915a 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -1,3 +1,5 @@ +.. _tutorial-testing: + Bonus: Testing the Application =============================== diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 29be65fa..03c7709b 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -1,3 +1,5 @@ +.. _tutorial-views: + Step 5: The View Functions ========================== @@ -85,3 +87,5 @@ that case if the user was logged in. session.pop('logged_in', None) flash('You were logged out') return redirect(url_for('show_entries')) + +Continue with :ref:`tutorial-templates`. diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 07ffe4c7..4e01cecb 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -127,6 +127,7 @@ def user_timeline(username): follower.who_id = ? and follower.whom_id = ?''', [session['user_id'], profile_user['user_id']], one=True) is not None + broken_just_for_djangocon return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where user.user_id = message.author_id and user.user_id = ? From 1264c458ae808d306aa03c51f70103e52dca107b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 26 May 2010 15:19:08 +0200 Subject: [PATCH 0219/3747] Document ways to generate secret keys. This fixes #47. --- docs/quickstart.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index cb887251..a2cbb8b5 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -659,6 +659,19 @@ sessions work:: The here mentioned :func:`~flask.escape` does escaping for you if you are not using the template engine (like in this example). +.. admonition:: How to generate good Secret Keys + + The problem with random is that it's hard to judge what random is. And + a secret key should be as random as possible. Your operating system + has ways to generate pretty random stuff based on a cryptographical + random generator which can be used to get such a key: + + >>> import os + >>> os.urandom(24) + '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O Date: Wed, 26 May 2010 15:30:06 +0200 Subject: [PATCH 0220/3747] Added versionadded for logger --- flask.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask.py b/flask.py index cbb10b59..fb595067 100644 --- a/flask.py +++ b/flask.py @@ -784,6 +784,8 @@ class Flask(_PackageBoundObject): app.logger.debug('A value for debugging') app.logger.warning('A warning ocurred (%d apples)', 42) app.logger.error('An error occoured') + + .. versionadded:: 0.5 """ from logging import getLogger, StreamHandler, Formatter, DEBUG class DebugHandler(StreamHandler): From 8d356d7cda963e2fa6437c5a5fef035e13cc80c5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 12:17:07 +0200 Subject: [PATCH 0221/3747] Fixed a build error and added a touch icon --- docs/_static/touch-icon.png | Bin 0 -> 3624 bytes docs/_themes | 2 +- docs/conf.py | 4 +++- docs/deploying/mod_wsgi.rst | 2 -- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/_static/touch-icon.png diff --git a/docs/_static/touch-icon.png b/docs/_static/touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd1e91e155bc4058f93f8e49dcac87de573bf89d GIT binary patch literal 3624 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 Date: Thu, 27 May 2010 13:30:54 +0200 Subject: [PATCH 0222/3747] Fixed stuff I broke --- examples/minitwit/minitwit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 4e01cecb..07ffe4c7 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -127,7 +127,6 @@ def user_timeline(username): follower.who_id = ? and follower.whom_id = ?''', [session['user_id'], profile_user['user_id']], one=True) is not None - broken_just_for_djangocon return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where user.user_id = message.author_id and user.user_id = ? From 68278fd78e15e3ef6a591f3f2b6cc700d1e4d6dd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 01:41:07 +0200 Subject: [PATCH 0223/3747] Started working on config support --- flask.py | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/flask.py b/flask.py index fb595067..a7d6c25c 100644 --- a/flask.py +++ b/flask.py @@ -615,6 +615,21 @@ class Module(_PackageBoundObject): self._register_events.append(func) +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name): + self.__name__ = name + + def __get__(self, obj, type=None): + if obj is None: + return self + return obj.config[self.__name__] + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -648,25 +663,31 @@ class Flask(_PackageBoundObject): #: and the development server will no longer serve any static files. static_path = '/static' + #: the debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + debug = ConfigAttribute('debug') + #: if a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. - secret_key = None + secret_key = ConfigAttribute('secret_key') #: The secure cookie uses this for the name of the session cookie - session_cookie_name = 'session' + session_cookie_name = ConfigAttribute('session.cookie_name') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. - permanent_session_lifetime = timedelta(days=31) + permanent_session_lifetime = ConfigAttribute('session.permanent_lifetime') #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files #: sent with the :func:`send_file` method. #: #: .. versionadded:: 0.2 - use_x_sendfile = False + use_x_sendfile = ConfigAttribute('use_x_sendfile') #: the logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging @@ -686,15 +707,22 @@ class Flask(_PackageBoundObject): extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) - def __init__(self, import_name): + #: default configuration parameters + default_config = ImmutableDict({ + 'debug': False, + 'secret_key': None, + 'session.cookie_name': 'session', + 'session.permanent_lifetime': timedelta(days=31), + 'use_x_sendfile': False + }) + + def __init__(self, import_name, config=None): _PackageBoundObject.__init__(self, import_name) - #: the debug flag. Set this to `True` to enable debugging of - #: the application. In debug mode the debugger will kick in - #: when an unhandled exception ocurrs and the integrated server - #: will automatically reload the application if changes in the - #: code are detected. - self.debug = False + #: the configuration dictionary + self.config = self.default_config.copy() + if config: + self.config.update(config) #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and From 4d16486132c349cdc7d473ef3c238dfa3f9d5385 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 18 May 2010 02:36:50 +0200 Subject: [PATCH 0224/3747] Improved configuration support. --- docs/api.rst | 5 +++ flask.py | 105 ++++++++++++++++++++++++++++++++++++++----- tests/flask_tests.py | 24 ++++++++++ 3 files changed, 124 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2225ee07..38c2321d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -284,3 +284,8 @@ Template Rendering .. autofunction:: render_template_string .. autofunction:: get_template_attribute + +Configuration +------------- + +.. autoclass:: Config diff --git a/flask.py b/flask.py index a7d6c25c..c90a42e5 100644 --- a/flask.py +++ b/flask.py @@ -19,7 +19,8 @@ from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers + ImmutableDict, cached_property, wrap_file, Headers, \ + import_string from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError from werkzeug.contrib.securecookie import SecureCookie @@ -630,6 +631,90 @@ class ConfigAttribute(object): obj.config[self.__name__] = value +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_module` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_module(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. The actual keys in the + config are however lowercased so they are converted for you. This makes + it possible to use lowercase values in the config file for temporary + values that are not added to the config or to define the config keys in + the same file that implements the application. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_pyfile(self, filename): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_module` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + """ + filename = os.path.join(self.root_path, filename) + d = type(sys)('config') + d.__file__ = filename + execfile(filename, d.__dict__) + self.from_module(d) + + def from_module(self, module): + """Updates the values from the given module. A module can be of one + of the following two types: + + - a string: in this case the module with that name will be imported + - an actual module reference: that module is used directly + + Just the uppercase variables in that module are stored in the config + after lowercasing. Example usage:: + + app.config.from_module('yourapplication.default_config') + from yourapplication import default_config + app.config.from_module(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth;`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param module: an import name or module + """ + if isinstance(module, basestring): + d = import_string(module).__dict__ + else: + d = module.__dict__ + for key, value in d.iteritems(): + if key.isupper(): + self[key.lower()] = value + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -675,12 +760,12 @@ class Flask(_PackageBoundObject): secret_key = ConfigAttribute('secret_key') #: The secure cookie uses this for the name of the session cookie - session_cookie_name = ConfigAttribute('session.cookie_name') + session_cookie_name = ConfigAttribute('session_cookie_name') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. - permanent_session_lifetime = ConfigAttribute('session.permanent_lifetime') + permanent_session_lifetime = ConfigAttribute('permanent_session_lifetime') #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files @@ -711,18 +796,18 @@ class Flask(_PackageBoundObject): default_config = ImmutableDict({ 'debug': False, 'secret_key': None, - 'session.cookie_name': 'session', - 'session.permanent_lifetime': timedelta(days=31), + 'session_cookie_name': 'session', + 'permanent_session_lifetime': timedelta(days=31), 'use_x_sendfile': False }) - def __init__(self, import_name, config=None): + def __init__(self, import_name): _PackageBoundObject.__init__(self, import_name) - #: the configuration dictionary - self.config = self.default_config.copy() - if config: - self.config.update(config) + #: the configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = Config(self.root_path, self.default_config) #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 17f73ad7..b1f9cdac 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -27,6 +27,11 @@ sys.path.append(os.path.join(example_path, 'flaskr')) sys.path.append(os.path.join(example_path, 'minitwit')) +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + @contextmanager def catch_stderr(): old_stderr = sys.stderr @@ -686,6 +691,24 @@ class LoggingTestCase(unittest.TestCase): assert rv.data == 'Hello Server Error' +class ConfigTestCase(unittest.TestCase): + + def common_module_test(self, app): + assert app.secret_key == 'devkey' + assert app.config['test_key'] == 'foo' + assert 'ConfigTestCase' not in app.config + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile('flask_tests.py') + self.common_module_test(app) + + def test_config_from_module(self): + app = flask.Flask(__name__) + app.config.from_module(__name__) + self.common_module_test(app) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -696,6 +719,7 @@ def suite(): suite.addTest(unittest.makeSuite(ModuleTestCase)) suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) + suite.addTest(unittest.makeSuite(ConfigTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From 35fd6eb22c4dec893770dd5720a51608a06fb8cd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 13:31:22 +0200 Subject: [PATCH 0225/3747] Use uppercase for config and support any object. --- flask.py | 52 ++++++++++++++++++++++---------------------- tests/flask_tests.py | 19 +++++++++++----- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/flask.py b/flask.py index c90a42e5..84e3d916 100644 --- a/flask.py +++ b/flask.py @@ -680,36 +680,36 @@ class Config(dict): d = type(sys)('config') d.__file__ = filename execfile(filename, d.__dict__) - self.from_module(d) + self.from_object(d) - def from_module(self, module): - """Updates the values from the given module. A module can be of one + def from_object(self, obj): + """Updates the values from the given object. An object can be of one of the following two types: - - a string: in this case the module with that name will be imported - - an actual module reference: that module is used directly + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly - Just the uppercase variables in that module are stored in the config + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config after lowercasing. Example usage:: - app.config.from_module('yourapplication.default_config') + app.config.from_object('yourapplication.default_config') from yourapplication import default_config - app.config.from_module(default_config) + app.config.from_object(default_config) You should not use this function to load the actual configuration but rather configuration defaults. The actual config should be loaded - with :meth;`from_pyfile` and ideally from a location not within the + with :meth:`from_pyfile` and ideally from a location not within the package because the package might be installed system wide. - :param module: an import name or module + :param obj: an import name or object """ - if isinstance(module, basestring): - d = import_string(module).__dict__ - else: - d = module.__dict__ - for key, value in d.iteritems(): + if isinstance(obj, basestring): + obj = import_string(obj) + for key in dir(obj): if key.isupper(): - self[key.lower()] = value + self[key] = getattr(obj, key) def __repr__(self): return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) @@ -752,27 +752,27 @@ class Flask(_PackageBoundObject): #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload #: the application if changes in the code are detected. - debug = ConfigAttribute('debug') + debug = ConfigAttribute('DEBUG') #: if a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. - secret_key = ConfigAttribute('secret_key') + secret_key = ConfigAttribute('SECRET_KEY') #: The secure cookie uses this for the name of the session cookie - session_cookie_name = ConfigAttribute('session_cookie_name') + session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. - permanent_session_lifetime = ConfigAttribute('permanent_session_lifetime') + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files #: sent with the :func:`send_file` method. #: #: .. versionadded:: 0.2 - use_x_sendfile = ConfigAttribute('use_x_sendfile') + use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') #: the logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging @@ -794,11 +794,11 @@ class Flask(_PackageBoundObject): #: default configuration parameters default_config = ImmutableDict({ - 'debug': False, - 'secret_key': None, - 'session_cookie_name': 'session', - 'permanent_session_lifetime': timedelta(days=31), - 'use_x_sendfile': False + 'DEBUG': False, + 'SECRET_KEY': None, + 'SESSION_COOKIE_NAME': 'session', + 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), + 'USE_X_SENDFILE': False }) def __init__(self, import_name): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b1f9cdac..8ab5ed1d 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -693,20 +693,29 @@ class LoggingTestCase(unittest.TestCase): class ConfigTestCase(unittest.TestCase): - def common_module_test(self, app): + def common_object_test(self, app): assert app.secret_key == 'devkey' - assert app.config['test_key'] == 'foo' + assert app.config['TEST_KEY'] == 'foo' assert 'ConfigTestCase' not in app.config def test_config_from_file(self): app = flask.Flask(__name__) app.config.from_pyfile('flask_tests.py') - self.common_module_test(app) + self.common_object_test(app) def test_config_from_module(self): app = flask.Flask(__name__) - app.config.from_module(__name__) - self.common_module_test(app) + app.config.from_object(__name__) + self.common_object_test(app) + + def test_config_from_class(self): + class Base(object): + TEST_KEY = 'foo' + class Test(Base): + SECRET_KEY = 'devkey' + app = flask.Flask(__name__) + app.config.from_object(Test) + self.common_object_test(app) def suite(): From 99c6287ce2319224f3a188f75641a9443466ab85 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 13:54:40 +0200 Subject: [PATCH 0226/3747] appengine -> app engine --- docs/deploying/cgi.rst | 6 +++--- docs/patterns/caching.rst | 2 +- docs/patterns/lazyloading.rst | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 1168249c..c0b8c560 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -6,7 +6,7 @@ is supported by all major servers but usually has a less-than-optimal performance. This is also the way you can use a Flask application on Google's -`AppEngine`_, there however the execution does happen in a CGI-like +`App Engine`_, there however the execution does happen in a CGI-like environment. The application's performance is unaffected because of that. .. admonition:: Watch Out @@ -15,9 +15,9 @@ environment. The application's performance is unaffected because of that. have in your application file, is inside an ``if __name__ == '__main__':`` or moved to a separate file. Just make sure it's not called because this will always start a local WSGI server which we do - not want if we deploy that application to CGI / appengine. + not want if we deploy that application to CGI / app engine. -.. _AppEngine: http://code.google.com/appengine/ +.. _App Engine: http://code.google.com/appengine/ Creating a `.cgi` file ---------------------- diff --git a/docs/patterns/caching.rst b/docs/patterns/caching.rst index 7a811b82..5817aa29 100644 --- a/docs/patterns/caching.rst +++ b/docs/patterns/caching.rst @@ -34,7 +34,7 @@ memcached server then:: from werkzeug.contrib.cache import MemcachedCache cache = MemcachedCache(['127.0.0.1:11211']) -If you are using appengine, you can connect to the appengine memcache +If you are using App Engine, you can connect to the App Engine memcache server easily:: from werkzeug.contrib.cache import GAEMemcachedCache diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index 2c331ca9..b1fe4cbf 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -8,7 +8,7 @@ that uses decorators has to be imported upfront or Flask will never actually find your function. This can be a problem if your application has to import quick. It might -have to do that on systems like Google's AppEngine or other systems. So +have to do that on systems like Google's App Engine or other systems. So if you suddenly notice that your application outgrows this approach you can fall back to a centralized URL mapping. From 182ee31503ea8a9eefd0d7063bd7fbdde991e13b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 17:42:01 +0200 Subject: [PATCH 0227/3747] Added chapter about config --- docs/api.rst | 1 + docs/config.rst | 120 +++++++++++++++++++++++++++++++++++ docs/contents.rst.inc | 1 + docs/patterns/distribute.rst | 2 + flask.py | 67 ++++++++++++++++--- 5 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 docs/config.rst diff --git a/docs/api.rst b/docs/api.rst index 38c2321d..f6920199 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -289,3 +289,4 @@ Configuration ------------- .. autoclass:: Config + :members: diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 00000000..24bc8b04 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1,120 @@ +Configuration Handling +====================== + +.. versionadded:: 0.5 + +Applications need some kind of configuration. There are different things +you might want to change. Like toggling debug mode, the secret key and a +lot of very similar things. + +The way Flask is designed usually requires the configuration to be +available when the application starts up. You can either hardcode the +configuration in the code which for many small applications is not +actually that bad, but there are better ways. + +Independent of how you load your config, there is a config object +available which holds the loaded configuration values: +The :attr:`~flask.Flask.config` attribute of the :class:`~flask.Flask` +object. This is the place where Flask itself puts certain configuration +values and also where extensions can put their configuration values. But +this is also where you can have your own configuration. + +Configuration Basics +-------------------- + +The :attr:`~flask.Flask.config` is actually a subclass of a dictionary and +can be modified just like any dictionary:: + + app = Flask(__name__) + app.config['DEBUG'] = True + +Certain configuration values are also forwarded to the +:attr:`~flask.Flask` object so that you can read and write them from +there:: + + app.debug = True + +To update multiple keys at once you can use the :meth:`dict.update` +method:: + + app.config.update( + DEBUG=True, + SECRET_KEY='...' + ) + +Builtin Configuration Values +---------------------------- + +The following configuration values are used internally by Flask: + +=============================== ========================================= +``DEBUG`` enable/disable debug mode +``SECRET_KEY`` the secret key +``SESSION_COOKIE_NAME`` the name of the session cookie +``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as + :class:`datetime.timedelta` object. +``USE_X_SENDFILE`` enable/disable x-sendfile +=============================== ========================================= + +Configuring from Files +---------------------- + +Configuration becomes more useful if you can configure from a file. And +ideally that file would be outside of the actual application package that +you can install the package with distribute (:ref:`distribute-deployment`) +and still modify that file afterwards. + +So a common pattern is this:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +What this does is first loading the configuration from the +`yourapplication.default_settings` module and then overrides the values +with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` +environment variable points to. This environment variable can be set on +Linux or OS X with the export command in the shell before starting the +server:: + + $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg + $ python run-app.py + * Running on http://127.0.0.1:5000/ + * Restarting with reloader... + +On Windows systems use the `set` builtin instead:: + + >set YOURAPPLICATION_SETTINGS=\path\to\settings.cfg + +The configuration files themselves are actual Python files. Only values +in uppercase are actually stored in the config object later on. So make +sure to use uppercase letters for your config keys. + +Here an example configuration file:: + + DEBUG = False + SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' + +Make sure to load the configuration very early on so that extensions have +the ability to access the configuration when starting up. There are other +methods on the config object as well to load from individual files. For a +complete reference, read the :class:`~flask.Config` object's +documentation. + + +Configuration Best Practices +---------------------------- + +The downside with the approach mentioned earlier is that it makes testing +a little harder. There is no one 100% solution for this problem in +general, but there are a couple of things you can do to improve that +experience: + +1. create your application in a function and register modules on it. + That way you can create multiple instances of your application with + different configurations attached which makes unittesting a lot + easier. You can use this to pass in configuration as needed. + +2. Do not write code that needs the configuration at import time. If you + limit yourself to request-only accesses to the configuration you can + reconfigure the object later on as needed. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 66dc0ead..5fda5931 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -14,6 +14,7 @@ web development. tutorial/index testing errorhandling + config shell patterns/index deploying/index diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index 2826550c..a3217f6e 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -1,3 +1,5 @@ +.. _distribute-deployment: + Deploying with Distribute ========================= diff --git a/flask.py b/flask.py index 84e3d916..077414d5 100644 --- a/flask.py +++ b/flask.py @@ -641,21 +641,33 @@ class Config(dict): app.config.from_pyfile('yourconfig.cfg') Or alternatively you can define the configuration options in the - module that calls :meth:`from_module` or provide an import path to + module that calls :meth:`from_object` or provide an import path to a module that should be loaded. It is also possible to tell it to use the same module and with that provide the configuration values just before the call:: DEBUG = True SECRET_KEY = 'development key' - app.config.from_module(__name__) + app.config.from_object(__name__) In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. The actual keys in the - config are however lowercased so they are converted for you. This makes - it possible to use lowercase values in the config file for temporary - values that are not added to the config or to define the config keys in - the same file that implements the application. + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. :param root_path: path to which files are read relative from. When the config object is created by the application, this is @@ -667,10 +679,33 @@ class Config(dict): dict.__init__(self, defaults or {}) self.root_path = root_path + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This basically is just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to `True` if you want silent failing for missing + files. + :return: bool. `True` if able to load config, `False` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file') + self.from_pyfile(rv) + return True + def from_pyfile(self, filename): """Updates the values in the config from a Python file. This function behaves as if the file was imported as module with the - :meth:`from_module` function. + :meth:`from_object` function. :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the @@ -752,19 +787,32 @@ class Flask(_PackageBoundObject): #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload #: the application if changes in the code are detected. + #: + #: This attribute can also be configured from the config with the `DEBUG` + #: configuration key. Defaults to `False`. debug = ConfigAttribute('DEBUG') #: if a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: `SECRET_KEY` configuration key. Defaults to `None`. secret_key = ConfigAttribute('SECRET_KEY') #: The secure cookie uses this for the name of the session cookie + #: + #: This attribute can also be configured from the config with the + #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') #: A :class:`~datetime.timedelta` which is used to set the expiration #: date of a permanent session. The default is 31 days which makes a #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') #: Enable this if you want to use the X-Sendfile feature. Keep in @@ -772,6 +820,9 @@ class Flask(_PackageBoundObject): #: sent with the :func:`send_file` method. #: #: .. versionadded:: 0.2 + #: + #: This attribute can also be configured from the config with the + #: `USE_X_SENDFILE` configuration key. Defaults to `False`. use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') #: the logging format used for the debug logger. This is only used when From 02b916d509422a4703df5110110686288666d3d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 17:55:57 +0200 Subject: [PATCH 0228/3747] Added appfactory pattern --- docs/patterns/appfactories.rst | 73 ++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 2 files changed, 74 insertions(+) create mode 100644 docs/patterns/appfactories.rst diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst new file mode 100644 index 00000000..27c53a75 --- /dev/null +++ b/docs/patterns/appfactories.rst @@ -0,0 +1,73 @@ +Application Factories +===================== + +If you are already using packages and modules for your application +(:ref:`packages`) there are couple of really nice ways to further improve +the experience. A common pattern is creating the application object when +the module is imported. But if you move the creation of this object, +into a function, you can then create multiple instances of this and later. + +So why would you want to do this? + +1. Testing. You can have instances of the application with different + settings to test every case. +2. Multiple instances. Imagine you want to run different versions of the + same application. Of course you could have multiple instances with + different configs set up in your webserver, but if you use factories, + you can have multiple instances of the same application running in the + same application process which can be handy. + +So how would you then actually implement that? + +Basic Factories +--------------- + +The idea is to set up the application in a function. Like this:: + + def create_app(config_filename): + app = Flask(__name__) + app.config.from_pyfile(config_filename) + + from yourapplication.views.admin import admin + from yourapplication.views.frontend import frontend + app.register_module(admin) + app.register_module(frontend) + + return app + +The downside is that you cannot use the application object in the modules +at import time. You can however use it from within a request. How do you +get access the application with the config? Use +:data:`~flask.current_app`:: + + from flask import current_app, Module, render_template + admin = Module(__name__, url_prefix='/admin') + + @admin.route('/') + def index(): + return render_template(current_app.config['INDEX_TEMPLATE']) + +Here we look up the name of a template in the config. + +Using Applications +------------------ + +So to use such an application you then have to create the application +first. Here an example `run.py` file that runs such an application:: + + from yourapplication import create_app + app = create_app('/path/to/config.cfg') + app.run() + +Factory Improvements +-------------------- + +The factory function from above is not very clever so far, you can improve +it. The following changes are straightforward and possible: + +1. make it possible to pass in configuration values for unittests so that + you don't have to create config files on the filesystem +2. call a function from a module when the application is setting up so + that you have a place to modify attributes of the application (like + hooking in before / after request handlers etc.) +3. Add in WSGI middlewares when the application is creating if necessary. diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index fcd482d7..ecc2c40f 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -17,6 +17,7 @@ Snippet Archives `_. :maxdepth: 2 packages + appfactories distribute sqlite3 sqlalchemy From dfecc86dd315586bec8f5f0838195d03ba7165d3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 27 May 2010 21:17:25 +0200 Subject: [PATCH 0229/3747] Ported examples over to new config. documented upgrading --- docs/_themes | 2 +- docs/becomingbig.rst | 1 - docs/config.rst | 4 +++ docs/contents.rst.inc | 1 + docs/foreword.rst | 5 --- docs/patterns/appfactories.rst | 2 ++ docs/patterns/jquery.rst | 2 +- docs/testing.rst | 14 +++++--- docs/tutorial/setup.rst | 23 +++++++++--- docs/tutorial/views.rst | 4 +-- docs/upgrading.rst | 56 +++++++++++++++++++++++++++++ examples/flaskr/README | 4 ++- examples/flaskr/flaskr.py | 10 +++--- examples/flaskr/flaskr_tests.py | 16 +++++---- examples/minitwit/README | 4 ++- examples/minitwit/minitwit.py | 9 +++-- examples/minitwit/minitwit_tests.py | 4 +-- 17 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 docs/upgrading.rst diff --git a/docs/_themes b/docs/_themes index f87d00ee..0d8f3d85 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit f87d00eee80e4a555e94ed124a94ffea483dc329 +Subproject commit 0d8f3d85558168647632c768bdea7d58cf6f8e42 diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index fcffe7c2..8ad125de 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -27,7 +27,6 @@ endpoints. Here are some suggestions for how Flask can be modified to better accommodate large-scale applications: -- implement dotted names for URL endpoints - get rid of the decorator function registering which causes a lot of troubles for applications that have circular dependencies. It also requires that the whole application is imported when the system diff --git a/docs/config.rst b/docs/config.rst index 24bc8b04..6a2900a1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1,3 +1,5 @@ +.. _config: + Configuration Handling ====================== @@ -47,6 +49,8 @@ Builtin Configuration Values The following configuration values are used internally by Flask: +.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| + =============================== ========================================= ``DEBUG`` enable/disable debug mode ``SECRET_KEY`` the secret key diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 5fda5931..918ad804 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -41,4 +41,5 @@ Design notes, legal information and changelog are here for the interested. design license + upgrading changelog diff --git a/docs/foreword.rst b/docs/foreword.rst index 34992a18..fe466dab 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -84,11 +84,6 @@ widespread usage. Recent versions of Flask scale nicely within reasonable bounds and if you grow larger, you won't have any troubles adjusting Flask for your new application size. -Flask serves two purposes: it's an example of how to create a minimal and -opinionated framework on top of Werkzeug to show how this can be done, and -to provide people with a simple tool to prototype larger applications or -to implement small and medium sized applications. - If you suddenly discover that your application grows larger than originally intended, head over to the :ref:`becomingbig` section to see some possible solutions for larger applications. diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 27c53a75..448dba71 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -1,3 +1,5 @@ +.. _app-factories: + Application Factories ===================== diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index f087e6f0..e9c9f956 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -77,7 +77,7 @@ inside a `script` block here where different rules apply. will not be parsed. Everything until ```` is handled as script. This also means that there must never be any ``"|tojson|safe }`` is rendered as + escape slashes for you (``{{ ""|tojson|safe }}`` is rendered as ``"<\/script>"``). diff --git a/docs/testing.rst b/docs/testing.rst index be72e746..2a58efd3 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -94,7 +94,7 @@ this:: class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.db_fd, flaskr.DATABASE = tempfile.mkstemp() + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() @@ -151,13 +151,13 @@ Now we can easily test if logging in and out works and that it fails with invalid credentials. Add this new test to the class:: def test_login_logout(self): - rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) + rv = self.login('admin', 'default') assert 'You were logged in' in rv.data rv = self.logout() assert 'You were logged out' in rv.data - rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) + rv = self.login('adminx', 'default') assert 'Invalid username' in rv.data - rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') + rv = self.login('admin', 'defaultx') assert 'Invalid password' in rv.data Test Adding Messages @@ -167,7 +167,7 @@ Now we can also test that adding messages works. Add a new test method like this:: def test_messages(self): - self.login(flaskr.USERNAME, flaskr.PASSWORD) + self.login('admin', 'default') rv = self.app.post('/add', data=dict( title='', text='HTML allowed here' @@ -214,3 +214,7 @@ functions. Here a full example that showcases this:: assert flask.request.args['name'] == 'Peter' All the other objects that are context bound can be used the same. + +If you want to test your application with different configurations and +there does not seem to be a good way to do that, consider switching to +application factories (see :ref:`app-factories`). diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index f9a0b302..790cc41e 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -26,12 +26,27 @@ the values from there. PASSWORD = 'default' Next we can create our actual application and initialize it with the -config:: +config from the same file:: # create our little application :) app = Flask(__name__) - app.secret_key = SECRET_KEY - app.debug = DEBUG + app.config.from_object(__name__) + +:meth:`~flask.Config.from_object` will look at the given object (if it's a +string it will import it) and then look for all uppercase variables +defined there. In our case, the configuration we just wrote a few lines +of code above. You can also move that into a separate file. + +It is also a good idea to be able to load a configuration from a +configurable file. This is what :meth:`~flask.Config.from_envvar` can +do:: + + app.config.from_envvar('FLASKR_SETTINGS', silent=True) + +That way someone can set an environment variable called +:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will +then override the default values. The silent switch just tells Flask to +not complain if no such environment key is set. The `secret_key` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The @@ -46,7 +61,7 @@ Python shell or a script. This will come in handy later :: def connect_db(): - return sqlite3.connect(DATABASE) + return sqlite3.connect(app.config['DATABASE']) Finally we just add a line to the bottom of the file that fires up the server if we run that file as standalone application:: diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 03c7709b..c7cc1ed4 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -63,9 +63,9 @@ notified about that and the user asked again:: def login(): error = None if request.method == 'POST': - if request.form['username'] != USERNAME: + if request.form['username'] != app.config['USERNAME']: error = 'Invalid username' - elif request.form['password'] != PASSWORD: + elif request.form['password'] != app.config['PASSWORD']: error = 'Invalid password' else: session['logged_in'] = True diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 00000000..e0f210db --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,56 @@ +Upgrading to Newer Releases +=========================== + +Flask itself is changing like any software is changing over time. Most of +the changes are the nice kind, the kind where you don't have th change +anything in your code to profit from a new release. + +However every once in a while there are changes that do require some +changes in your code or there are changes that make it possible for you to +improve your own code quality by taking advantage of new features in +Flask. + +This section of the documentation enumerates all the changes in Flask from +release to release and how you can change your code to have a painless +updating experience. + +Version 0.5 +----------- + +Flask 0.5 introduces configuration support and logging as well as +categories for flashing messages. All these are features that are 100% +backwards compatible but you might want to take advantage of them. + +Configuration Support +````````````````````` + +The configuration support makes it easier to write any kind of application +that requires some sort of configuration. (Which most likely is the case +for any application out there). + +If you previously had code like this:: + + app.debug = DEBUG + app.secret_key = SECRET_KEY + +You no longer have to do that, instead you can just load a configuration +into the config object. How this works is outlined in :ref:`config`. + +Logging Integration +``````````````````` + +Flask now configures a logger for you with some basic and useful defaults. +If you run your application in production and want to profit from +automatic error logging, you might be interested in attaching a proper log +handler. Also you can start logging warnings and errors into the logger +when appropriately. For more information on that, read +:ref:`application-errors`. + +Categories for Flash Messages +````````````````````````````` + +Flash messages can now have categories attached. This makes it possible +to render errors, warnings or regular messages differently for example. +This is an opt-in feature because it requires some rethinking in the code. + +Read all about that in the :ref:`message-flashing-pattern` pattern. diff --git a/examples/flaskr/README b/examples/flaskr/README index 4a9a02c6..9ab20589 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -10,7 +10,9 @@ ~ How do I use it? - 1. edit the configuration in the flaskr.py file + 1. edit the configuration in the flaskr.py file or + export an FLASKR_SETTINGS environment variable + pointing to a configuration file. 2. fire up a python shell and run this: diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index f2a8b341..1df24293 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -24,13 +24,13 @@ PASSWORD = 'default' # create our little application :) app = Flask(__name__) -app.secret_key = SECRET_KEY -app.debug = DEBUG +app.config.from_object(__name__) +app.config.from_envvar('FLASKR_SETTINGS', silent=True) def connect_db(): """Returns a new connection to the database.""" - return sqlite3.connect(DATABASE) + return sqlite3.connect(app.config['DATABASE']) def init_db(): @@ -76,9 +76,9 @@ def add_entry(): def login(): error = None if request.method == 'POST': - if request.form['username'] != USERNAME: + if request.form['username'] != app.config['USERNAME']: error = 'Invalid username' - elif request.form['password'] != PASSWORD: + elif request.form['password'] != app.config['PASSWORD']: error = 'Invalid password' else: session['logged_in'] = True diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index 9421cca6..07e702c4 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -18,14 +18,14 @@ class FlaskrTestCase(unittest.TestCase): def setUp(self): """Before each test, set up a blank database""" - self.db_fd, flaskr.DATABASE = tempfile.mkstemp() + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() def tearDown(self): """Get rid of the database again after each test.""" os.close(self.db_fd) - os.unlink(flaskr.DATABASE) + os.unlink(flaskr.app.config['DATABASE']) def login(self, username, password): return self.app.post('/login', data=dict( @@ -45,18 +45,22 @@ class FlaskrTestCase(unittest.TestCase): def test_login_logout(self): """Make sure login and logout works""" - rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) + rv = self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) assert 'You were logged in' in rv.data rv = self.logout() assert 'You were logged out' in rv.data - rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) + rv = self.login(flaskr.app.config['USERNAME'] + 'x', + flaskr.app.config['PASSWORD']) assert 'Invalid username' in rv.data - rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') + rv = self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD'] + 'x') assert 'Invalid password' in rv.data def test_messages(self): """Test that messages work""" - self.login(flaskr.USERNAME, flaskr.PASSWORD) + self.login(flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) rv = self.app.post('/add', data=dict( title='', text='HTML allowed here' diff --git a/examples/minitwit/README b/examples/minitwit/README index 065674a9..ab946295 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -10,7 +10,9 @@ ~ How do I use it? - 1. edit the configuration in the minitwit.py file + 1. edit the configuration in the minitwit.py file or + export an MINITWIT_SETTINGS environment variable + pointing to a configuration file. 2. fire up a python shell and run this: diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 07ffe4c7..b740bc25 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -27,11 +27,13 @@ SECRET_KEY = 'development key' # create our little application :) app = Flask(__name__) +app.config.from_object(__name__) +app.config.from_envvar('MINITWIT_SETTINGS', silent=True) def connect_db(): """Returns a new connection to the database.""" - return sqlite3.connect(DATABASE) + return sqlite3.connect(app.config['DATABASE']) def init_db(): @@ -237,12 +239,9 @@ def logout(): return redirect(url_for('public_timeline')) -# add some filters to jinja and set the secret key and debug mode -# from the configuration. +# add some filters to jinja app.jinja_env.filters['datetimeformat'] = format_datetime app.jinja_env.filters['gravatar'] = gravatar_url -app.secret_key = SECRET_KEY -app.debug = DEBUG if __name__ == '__main__': diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py index 10962142..87741165 100644 --- a/examples/minitwit/minitwit_tests.py +++ b/examples/minitwit/minitwit_tests.py @@ -18,14 +18,14 @@ class MiniTwitTestCase(unittest.TestCase): def setUp(self): """Before each test, set up a blank database""" - self.db_fd, minitwit.DATABASE = tempfile.mkstemp() + self.db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() self.app = minitwit.app.test_client() minitwit.init_db() def tearDown(self): """Get rid of the database again after each test.""" os.close(self.db_fd) - os.unlink(minitwit.DATABASE) + os.unlink(minitwit.app.config['DATABASE']) # helper functions From ce6e4cbd73d57cb8c1bba85c46490f71061f865f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 01:18:29 +0200 Subject: [PATCH 0230/3747] 0.5 is 0.3 now, why skip numbers? --- CHANGES | 2 +- docs/config.rst | 2 +- docs/errorhandling.rst | 2 +- docs/patterns/flashing.rst | 2 +- docs/quickstart.rst | 4 ++-- docs/shell.rst | 2 +- docs/upgrading.rst | 4 ++-- flask.py | 14 +++++++------- setup.py | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGES b/CHANGES index 47dd08b6..c44fd9be 100644 --- a/CHANGES +++ b/CHANGES @@ -3,7 +3,7 @@ Flask Changelog Here you can see the full list of changes between each Flask release. -Version 0.5 +Version 0.3 ----------- Release date to be announced diff --git a/docs/config.rst b/docs/config.rst index 6a2900a1..de79cabc 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -3,7 +3,7 @@ Configuration Handling ====================== -.. versionadded:: 0.5 +.. versionadded:: 0.3 Applications need some kind of configuration. There are different things you might want to change. Like toggling debug mode, the secret key and a diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index dcc2a2e9..f963f1ab 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -3,7 +3,7 @@ Handling Application Errors =========================== -.. versionadded:: 0.5 +.. versionadded:: 0.3 Applications fail, server fail. Sooner or later you will see an exception in production. Even if your code is 100% correct, you will still see diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 16d8d371..b491712f 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -86,7 +86,7 @@ And of course the login template: Flashing With Categories ------------------------ -.. versionadded:: 0.5 +.. versionadded:: 0.3 It is also possible to provide categories when flashing a message. The default category if nothing is provided is ``'message'``. Alternative diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a2cbb8b5..a3cc4793 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -691,7 +691,7 @@ for a full example. Logging ------- -.. versionadded:: 0.5 +.. versionadded:: 0.3 Sometimes you might be in the situation where you deal with data that should be correct, but actually is not. For example you have some client @@ -702,7 +702,7 @@ Request`` in that situation, but other times it is not and the code has to continue working. Yet you want to log that something fishy happened. This is where loggers -come in handy. As of Flask 0.5 a logger is preconfigured for you to use. +come in handy. As of Flask 0.3 a logger is preconfigured for you to use. Here are some example log calls:: diff --git a/docs/shell.rst b/docs/shell.rst index 65e44ce7..470bceca 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,7 +1,7 @@ Working with the Shell ====================== -.. versionadded:: 0.5 +.. versionadded:: 0.3 One of the reasons everybody loves Python is the interactive shell. It basically allows you to execute Python commands in real time and diff --git a/docs/upgrading.rst b/docs/upgrading.rst index e0f210db..f06523e9 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -14,10 +14,10 @@ This section of the documentation enumerates all the changes in Flask from release to release and how you can change your code to have a painless updating experience. -Version 0.5 +Version 0.3 ----------- -Flask 0.5 introduces configuration support and logging as well as +Flask 0.3 introduces configuration support and logging as well as categories for flashing messages. All these are features that are 100% backwards compatible but you might want to take advantage of them. diff --git a/flask.py b/flask.py index 077414d5..5e3367bb 100644 --- a/flask.py +++ b/flask.py @@ -232,7 +232,7 @@ def flash(message, category='message'): flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. - .. versionchanged: 0.5 + .. versionchanged: 0.3 `category` parameter added. :param message: the message to be flashed. @@ -260,7 +260,7 @@ def get_flashed_messages(with_categories=False):

    {{ msg }} {% endfor %} - .. versionchanged:: 0.5 + .. versionchanged:: 0.3 `with_categories` parameter added. :param with_categories: set to `True` to also receive categories. @@ -829,7 +829,7 @@ class Flask(_PackageBoundObject): #: the application is in debug mode, otherwise the attached logging #: handler does the formatting. #: - #: .. versionadded:: 0.5 + #: .. versionadded:: 0.3 debug_log_format = ( '-' * 80 + '\n' + '%(levelname)s in %(module)s, %(pathname)s:%(lineno)d]:\n' + @@ -949,7 +949,7 @@ class Flask(_PackageBoundObject): app.logger.warning('A warning ocurred (%d apples)', 42) app.logger.error('An error occoured') - .. versionadded:: 0.5 + .. versionadded:: 0.3 """ from logging import getLogger, StreamHandler, Formatter, DEBUG class DebugHandler(StreamHandler): @@ -1225,7 +1225,7 @@ class Flask(_PackageBoundObject): registered error handlers and fall back to returning the exception as response. - .. versionadded: 0.5 + .. versionadded: 0.3 """ handler = self.error_handlers.get(e.code) if handler is None: @@ -1239,7 +1239,7 @@ class Flask(_PackageBoundObject): for an 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. - .. versionadded: 0.5 + .. versionadded: 0.3 """ handler = self.error_handlers.get(500) if self.debug: @@ -1397,7 +1397,7 @@ class Flask(_PackageBoundObject): u'/' >>> ctx.unbind() - .. versionchanged:: 0.5 + .. versionchanged:: 0.3 Added support for non-with statement usage and `with` statement is now passed the ctx object. diff --git a/setup.py b/setup.py index ed922027..280c4f2a 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ from setuptools import setup setup( name='Flask', - version='0.5', + version='0.3', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From d29a9765a5b7014d3a52272f80ee90f4d98cf799 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 01:25:19 +0200 Subject: [PATCH 0231/3747] HEAD is 0.4dev --- CHANGES | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c44fd9be..8bd22de5 100644 --- a/CHANGES +++ b/CHANGES @@ -3,10 +3,15 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.4 +----------- + +Release date to be announced, codename to be selected. + Version 0.3 ----------- -Release date to be announced +Released on May 28th, codename Schnaps - added support for categories for flashed messages. - the application now configures a :class:`logging.Handler` and will From 4ca1d0a2ed7639d3f1d2e466a777f7e6b0e8aa0a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 01:25:35 +0200 Subject: [PATCH 0232/3747] update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 280c4f2a..66ef6a66 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ from setuptools import setup setup( name='Flask', - version='0.3', + version='0.4', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 6913dc173303de9534c32d716c22b7f371ab6303 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 01:30:46 +0200 Subject: [PATCH 0233/3747] Added missing changelog entry --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 8bd22de5..d01577cc 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,7 @@ Released on May 28th, codename Schnaps the with statement for playing in the console. - the request context is now available within the with statement making it possible to further push the request context or pop it. +- added support for configurations. Version 0.2 ----------- From 6cb0855e2dd181df5353fc1be9b437c7bc92eb2f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 21:06:39 +0200 Subject: [PATCH 0234/3747] Fixed an error reporting bug with flask.Config.from_envvar --- CHANGES | 7 +++++++ flask.py | 3 ++- tests/flask_tests.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index d01577cc..451ec9af 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,13 @@ Version 0.4 Release date to be announced, codename to be selected. +Version 0.3.1 +------------- + +Bugfix release, released May 28th + +- fixed a error reporting bug with :meth:`flask.Config.from_envvar` + Version 0.3 ----------- diff --git a/flask.py b/flask.py index 5e3367bb..a8e06bd3 100644 --- a/flask.py +++ b/flask.py @@ -698,7 +698,8 @@ class Config(dict): raise RuntimeError('The environment variable %r is not set ' 'and as such configuration could not be ' 'loaded. Set this variable and make it ' - 'point to a configuration file') + 'point to a configuration file' % + variable_name) self.from_pyfile(rv) return True diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3673453a..ff95364f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -717,6 +717,26 @@ class ConfigTestCase(unittest.TestCase): app.config.from_object(Test) self.common_object_test(app) + def test_config_from_envvar(self): + import os + env = os.environ + try: + os.environ = {} + app = flask.Flask(__name__) + try: + app.config.from_envvar('FOO_SETTINGS') + except RuntimeError, e: + assert "'FOO_SETTINGS' is not set" in str(e) + else: + assert 0, 'expected exception' + not app.config.from_envvar('FOO_SETTINGS', silent=True) + + os.environ = {'FOO_SETTINGS': 'flask_tests.py'} + assert app.config.from_envvar('FOO_SETTINGS') + self.common_object_test(app) + finally: + os.environ = env + def suite(): from minitwit_tests import MiniTwitTestCase From 9959ddd3da09f619d467891b194389851cf50367 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 21:07:06 +0200 Subject: [PATCH 0235/3747] Removed some unused code --- flask.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flask.py b/flask.py index a8e06bd3..bf4451c7 100644 --- a/flask.py +++ b/flask.py @@ -1424,8 +1424,3 @@ current_app = LocalProxy(lambda: _request_ctx_stack.top.app) request = LocalProxy(lambda: _request_ctx_stack.top.request) session = LocalProxy(lambda: _request_ctx_stack.top.session) g = LocalProxy(lambda: _request_ctx_stack.top.g) - - -# script interface to run a development server -if __name__ == '__main__': - sys.exit(main(sys.argv)) From dba42ae968a1cfa6cc6a3ead99ded554a710f041 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 21:07:28 +0200 Subject: [PATCH 0236/3747] Added missing message --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 451ec9af..d3fac153 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Version 0.3.1 Bugfix release, released May 28th - fixed a error reporting bug with :meth:`flask.Config.from_envvar` +- removed some unused code from flask Version 0.3 ----------- From 8e4bcaf57630b5449999594063ffe8dc104e316a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 28 May 2010 21:21:28 +0200 Subject: [PATCH 0237/3747] Fixed build process --- CHANGES | 3 +++ MANIFEST.in | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index d3fac153..98e94a1d 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,9 @@ Bugfix release, released May 28th - fixed a error reporting bug with :meth:`flask.Config.from_envvar` - removed some unused code from flask +- release does no longer include development leftover files (.git + folder for themes, built documentation in zip and pdf file and + some .pyc files) Version 0.3 ----------- diff --git a/MANIFEST.in b/MANIFEST.in index aee76e91..709e1791 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,10 @@ include Makefile CHANGES LICENSE AUTHORS recursive-include tests * +recursive-include examples * recursive-include docs * -prune docs/_build/doctrees +recursive-exclude docs *.pyc +recursive-exclude docs *.pyo +recursive-exclude examples *.pyc +recursive-exclude examples *.pyo +prune docs/_build +prune docs/_themes/.git From 63caf6e15882292832485372cd86b8265a007f92 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sun, 30 May 2010 01:53:08 +0800 Subject: [PATCH 0238/3747] Fix some typos. --- docs/design.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design.rst b/docs/design.rst index b2a56cf5..2a152d61 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -34,8 +34,8 @@ Would look like this instead:: return 'Hello World!' There are three major reasons for this. The most important one is that -implicit application objects require that there may only be one class at -the time. There are ways to fake multiple application with a single +implicit application objects require that there may only be one instance at +the time. There are ways to fake multiple applications with a single application object, like maintaining a stack of applications, but this causes some problems I won't outline here in detail. Now the question is: when does a microframework need more than one application at the same @@ -44,7 +44,7 @@ something it can be very helpful to create a minimal application to test specific behavior. When the application object is deleted everything it allocated will be freed again. -Another thing that becomes possible when you have an explicit object laying +Another thing that becomes possible when you have an explicit object lying around in your code is that you can subclass the base class (:class:`~flask.Flask`) to alter specific behaviour. This would not be possible without hacks if the object were created ahead of time for you From a224fecfd57a77c3f7279102dcc0b332a6858065 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sun, 30 May 2010 02:06:12 +0800 Subject: [PATCH 0239/3747] More typo fixes. --- docs/api.rst | 2 +- docs/deploying/mod_wsgi.rst | 8 ++++---- docs/design.rst | 6 +++--- docs/installation.rst | 2 +- docs/quickstart.rst | 18 +++++++++--------- docs/testing.rst | 6 +++--- docs/tutorial/css.rst | 2 +- docs/tutorial/dbinit.rst | 2 +- docs/tutorial/testing.rst | 2 +- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index f6920199..152c4cf7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -69,7 +69,7 @@ Incoming Request Data the data is stored unmodified in this stream for consumption. Most of the time it is a better idea to use :attr:`data` which will give you that data as a string. The stream only returns the data once. - + .. attribute:: data Contains the incoming request data as string in case it came with diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 1561a37c..b4753c3e 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -19,10 +19,10 @@ Installing `mod_wsgi` If you don't have `mod_wsgi` installed yet you have to either install it using a package manager or compile it yourself. -The mod_wsgi `installation instructions`_ cover source installations on UNIX +The mod_wsgi `installation instructions`_ cover source installations on UNIX systems. -If you are using ubuntu / debian you can apt-get it and activate it as follows: +If you are using Ubuntu/Debian you can apt-get it and activate it as follows: .. sourcecode:: text @@ -96,7 +96,7 @@ Toubleshooting If your application does not run, follow this guide to troubleshoot: -**Problem:** Application does not run, errorlog shows SystemExit ignored +**Problem:** application does not run, errorlog shows SystemExit ignored You have a ``app.run()`` call in your application file that is not guarded by an ``if __name__ == '__main__':`` condition. Either remove that :meth:`~flask.Flask.run` call from the file and move it into a @@ -130,6 +130,6 @@ If your application does not run, follow this guide to troubleshoot: instead you either have to put the folder into the pythonpath the file is stored in, or convert your application into a package. - The reason for this is that for non-installed Packages, the module + The reason for this is that for non-installed packages, the module filename is used to locate the resources and for symlinks the wrong filename is picked up. diff --git a/docs/design.rst b/docs/design.rst index 2a152d61..6a4674ba 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -90,15 +90,15 @@ of variables and take the return value as string. But that's about where similarities end. Jinja2 for example has an extensive filter system, a certain way to do template inheritance, support for reusable blocks (macros) that can be used from inside templates and -also from Python code, uses unicode for all operations, supports +also from Python code, uses Unicode for all operations, supports iterative template rendering, configurable syntax and more. On the other hand an engine like Genshi is based on XML stream evaluation, template inheritance by taking the availability of XPath into account and more. Mako on the other hand treats templates similar to Python modules. -When it comes to connecting a template engine with an application or +When it comes to connecting a template engine with an application or framework there is more than just rendering templates. For instance, -Flask uses Jinja2's extensive autoescaping support. Also it provides +Flask uses Jinja2's extensive autoescaping support. Also it provides ways to access macros from Jinja2 templates. A template abstraction layer that would not take the unique features of diff --git a/docs/installation.rst b/docs/installation.rst index 31c40023..78fb103f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -29,7 +29,7 @@ you have shell access. So first: what does virtualenv do? If you are like me and you like Python, chances are you want to use it for another project as well. Now the more projects you have, the more likely it is that you will be working with different versions of Python itself or at -least an individual library. Because let's face it: quite often libraries +least an individual library. Because let's face it: quite often libraries break backwards compatibility and it's unlikely that your application will not have any dependencies, that just won't happen. So virtualenv to the rescue! diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a3cc4793..1ee4c1ac 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -209,19 +209,19 @@ parameter. Here some examples: >>> app = Flask(__name__) >>> @app.route('/') ... def index(): pass -... +... >>> @app.route('/login') ... def login(): pass -... +... >>> @app.route('/user/') ... def profile(username): pass -... +... >>> with app.test_request_context(): ... print url_for('index') ... print url_for('login') ... print url_for('login', next='/') ... print url_for('profile', username='John Doe') -... +... / /login /login?next=/ @@ -238,7 +238,7 @@ templates? There are three good reasons for this: 1. reversing is often more descriptive than hardcoding the URLs. Also and more importantly you can change URLs in one go without having to change the URLs all over the place. -2. URL building will handle escaping of special characters and unicode +2. URL building will handle escaping of special characters and Unicode data transparently for you, you don't have to deal with that. 3. If your application is placed outside the URL root (so say in ``/myapplication`` instead of ``/``), :func:`~flask.url_for` will @@ -355,7 +355,7 @@ application is a module, that folder is next to that module, if it's a package it's actually inside your package: **Case 1**: a module:: - + /application.py /templates /hello.html @@ -663,7 +663,7 @@ not using the template engine (like in this example). The problem with random is that it's hard to judge what random is. And a secret key should be as random as possible. Your operating system - has ways to generate pretty random stuff based on a cryptographical + has ways to generate pretty random stuff based on a cryptographic random generator which can be used to get such a key: >>> import os @@ -707,8 +707,8 @@ come in handy. As of Flask 0.3 a logger is preconfigured for you to use. Here are some example log calls:: app.logger.debug('A value for debugging') - app.logger.warning('A warning ocurred (%d apples)', 42) - app.logger.error('An error occoured') + app.logger.warning('A warning occurred (%d apples)', 42) + app.logger.error('An error occurred') The attached :attr:`~flask.Flask.logger` is a standard logging :class:`~logging.Logger`, so head over to the official stdlib diff --git a/docs/testing.rst b/docs/testing.rst index 2a58efd3..db2b4188 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -70,7 +70,7 @@ low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. -If we now run that testsuite, we should see the following output:: +If we now run that test suite, we should see the following output:: $ python flaskr_tests.py @@ -181,11 +181,11 @@ which is the intended behavior. Running that should now give us three passing tests:: - $ python flaskr_tests.py + $ python flaskr_tests.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.332s - + OK For more complex tests with headers and status codes, check out the diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst index 76265a65..03f62ed1 100644 --- a/docs/tutorial/css.rst +++ b/docs/tutorial/css.rst @@ -14,7 +14,7 @@ folder we created before: h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } - + .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 602b999d..b546a1a8 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -31,7 +31,7 @@ first (`__future__` imports must be the very first import):: Next we can create a function called `init_db` that initializes the database. For this we can use the `connect_db` function we defined earlier. Just add that function below the `connect_db` function:: - + def init_db(): with closing(connect_db()) as db: with app.open_resource('schema.sql') as f: diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index 051e915a..ed9fc3e3 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -1,7 +1,7 @@ .. _tutorial-testing: Bonus: Testing the Application -=============================== +============================== Now that you have finished the application and everything works as expected, it's probably not a good idea to add automated tests to simplify From 7a4b6088395387abe4127c9650f9cbaac11a0ec4 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 31 May 2010 22:19:42 +0800 Subject: [PATCH 0240/3747] Add Module.app_errorhandler, like Flask.errorhandler. --- flask.py | 9 +++++++++ tests/flask_tests.py | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/flask.py b/flask.py index bf4451c7..66ce172f 100644 --- a/flask.py +++ b/flask.py @@ -612,6 +612,15 @@ class Module(_PackageBoundObject): .setdefault(None, []).append(f)) return f + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a module. This + handler is used for all requests, even if outside of the module. + """ + def decorator(f): + self._record(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator + def _record(self, func): self._register_events.append(func) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ff95364f..9d1dba6e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -260,7 +260,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.data == 'not found' rv = c.get('/error') assert rv.status_code == 500 - assert 'internal server error' in rv.data + assert 'internal server error' == rv.data def test_response_creation(self): app = flask.Flask(__name__) @@ -536,6 +536,30 @@ class ModuleTestCase(unittest.TestCase): app.register_module(admin, url_prefix='/admin') assert app.test_client().get('/admin/').data == '42' + def test_error_handling(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.app_errorhandler(404) + def not_found(e): + return 'not found', 404 + @admin.app_errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @admin.route('/') + def index(): + flask.abort(404) + @admin.route('/error') + def error(): + 1 // 0 + app.register_module(admin) + c = app.test_client() + rv = c.get('/') + assert rv.status_code == 404 + assert rv.data == 'not found' + rv = c.get('/error') + assert rv.status_code == 500 + assert 'internal server error' == rv.data + class SendfileTestCase(unittest.TestCase): From 50bca8c2d34b4b31ffcc98f6ced44b18de3c78b0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 31 May 2010 17:38:05 +0200 Subject: [PATCH 0241/3747] Updated AUTHORS file and added missing versionadded --- AUTHORS | 9 +++++++++ CHANGES | 3 +++ flask.py | 2 ++ 3 files changed, 14 insertions(+) diff --git a/AUTHORS b/AUTHORS index 2c125e11..e4f32ace 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,3 +12,12 @@ Patches and Suggestions - Chris Edgemon - Chris Grindstaff - Florent Xicluna +- Georg Brandl +- Justin Quick +- Kenneth Reitz +- Marian Sigler +- Ron DuPlain +- Sebastien Estienne +- Simon Sapin +- Stephane Wirtel +- Zhao Xiaohong diff --git a/CHANGES b/CHANGES index 98e94a1d..83d1af06 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Version 0.4 Release date to be announced, codename to be selected. +- added the ability to register application wide error handlers + from modules. + Version 0.3.1 ------------- diff --git a/flask.py b/flask.py index 66ce172f..71c30d84 100644 --- a/flask.py +++ b/flask.py @@ -615,6 +615,8 @@ class Module(_PackageBoundObject): def app_errorhandler(self, code): """Like :meth:`Flask.errorhandler` but for a module. This handler is used for all requests, even if outside of the module. + + .. versionadded:: 0.4 """ def decorator(f): self._record(lambda s: s.app.errorhandler(code)(f)) From a6617f44c0416c6877d930b312e1f64df08eca0e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 31 May 2010 17:50:31 +0200 Subject: [PATCH 0242/3747] Documented _request_ctx_stack. This fixes #57 --- docs/api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 152c4cf7..0fb50551 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -290,3 +290,13 @@ Configuration .. autoclass:: Config :members: + +Useful Internals +---------------- + +.. data:: _request_ctx_stack + + The internal :class:`~werkzeug.LocalStack` that is used to implement + all the context local objects used in Flask. This is a documented + instance and can be used by extensions and application code but the + use is discouraged in general. From 707b30749daf407c07b5b265bd892564576ad80a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 31 May 2010 17:53:10 +0200 Subject: [PATCH 0243/3747] use setup.py test. This fixes #56 --- Makefile | 2 +- setup.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b46bb3b5..4b1d8081 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: clean-pyc test test: - python tests/flask_tests.py + python setup.py test release: python setup.py release sdist upload diff --git a/setup.py b/setup.py index 66ef6a66..96fc77ba 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,13 @@ Links from setuptools import setup +def run_tests(): + import os, sys + sys.path.append(os.path.join(os.path.dirname(__file__), 'tests')) + from flask_tests import suite + return suite() + + setup( name='Flask', version='0.4', @@ -67,5 +74,6 @@ setup( 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' - ] + ], + test_suite='__main__.run_tests' ) From ee69fb58906818f50a2afe71e4ecde7af96b791a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 31 May 2010 21:14:27 +0200 Subject: [PATCH 0244/3747] Added document about extension development --- docs/_themes | 2 +- docs/contents.rst.inc | 1 + docs/extensiondev.rst | 276 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 docs/extensiondev.rst diff --git a/docs/_themes b/docs/_themes index 0d8f3d85..77a1db55 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit 0d8f3d85558168647632c768bdea7d58cf6f8e42 +Subproject commit 77a1db551aa956069ff4408b6c05814d86b0dc0d diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 918ad804..9a3ff5a8 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -40,6 +40,7 @@ Design notes, legal information and changelog are here for the interested. :maxdepth: 2 design + extensiondev license upgrading changelog diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst new file mode 100644 index 00000000..ca8a4ea4 --- /dev/null +++ b/docs/extensiondev.rst @@ -0,0 +1,276 @@ +Flask Extension Development +=========================== + +Flask, being a microframework, often requires some repetitive steps to get +a third party library working. Because very often these steps could be +abstracted to support multiple projects the `Flask Extension Registry`_ +was created. + +If you want to create your own Flask extension for something that does not +exist yet, this guide to extension development will help you get your +extension running in no time and to feel like users would expect your +extension to behave. + +.. _Flask Extension Registry: http://flask.pocoo.org/extensions/ + +Anatomy of an Extension +----------------------- + +Extensions are all located in a package called ``flaskext.something`` +where "something" is the name of the library you want to bridge. So for +example if you plan to add support for a library named `simplexml` to +Flask, you would name your extension's package ``flaskext.simplexml``. + +The name of the actual extension (the human readable name) however would +be something like "Flask-SimpleXML". Make sure to include the name +"Flask" somewhere in that name and that you check the capitalization. +This is how users can then register dependencies to your extension in +their `setup.py` files. + +The magic that makes it possible to have your library in a package called +``flaskext.something`` is called a "namespace package". Check out the +guide below how to create something like that. + +But how do extensions look like themselves? An extension has to ensure +that it works with multiple Flask application instances at once. This is +a requirement because many people will use patterns like the +:ref:`app-factories` pattern to create their application as needed to aid +unittests and to support multiple configurations. Because of that it is +crucial that your application supports that kind of behaviour. + +Most importantly the extension must be shipped with a `setup.py` file and +registered on PyPI. Also the development checkout link should work so +that people can easily install the development version into their +virtualenv without having to download the library by hand. + +Flask extensions must be licensed as BSD or MIT or a more liberal license +to be enlisted on the Flask Extension Registry. Keep in mind that the +Flask Extension Registry is a moderated place and libraries will be +reviewed upfront if they behave as required. + +"Hello Flaskext!" +----------------- + +So let's get started with creating such a Flask extension. The extension +we want to create here will provide very basic support for SQLite3. + +There is a script on github called `Flask Extension Wizard`_ which helps +you create the initial folder structure. But for this very basic example +we want to create all by hand to get a better feeling for it. + +First we create the following folder structure:: + + flask-sqlite3/ + flaskext/ + __init__.py + sqlite3.py + setup.py + LICENSE + +Here the contents of the most important files: + +flaskext/__init__.py +```````````````````` + +The only purpose of this file is to mark the package as namespace package. +This is required so that multiple modules from different PyPI packages can +reside in the same Python package:: + + __import__('pkg_resources').declare_namespace(__name__) + +If you want to know exactly what is happening there, checkout the +distribute or setuptools docs which explain how this works. + +Just make sure to not put anything else in there! + +setup.py +```````` + +The next file that is absolutely required is the `setup.py` file which is +used to install your Flask extension. The following contents are +something you can work with:: + + """ + Flask-SQLite3 + ------------- + + This is the description for that library + """ + from setuptools import setup + + + setup( + name='Flask-SQLite3', + version='1.0', + url='http://example.com/flask-sqlite3/', + license='BSD', + author='Your Name', + author_email='your-email@example.com', + description='Very short description', + long_description=__doc__, + packages=['flaskext'], + namespace_packages=['flaskext'], + zip_safe=False, + platforms='any', + install_requires=[ + 'Flask' + ], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] + ) + +That's a lot of code but you can really just copy/paste that from existing +extensions and adapt. This is also what the wizard creates for you if you +use it. + +flaskext/sqlite3.py +``````````````````` + +Now this is where your extension code goes. But how exactly should such +an extension look like? What are the best practices? Continue reading +for some insight. + + +Initializing Extensions +----------------------- + +Many extensions will need some kind of initialization step. For example, +consider your application is currently connecting to SQLite like the +documentation suggests (:ref:`sqlite3`) you will need to provide a few +functions and before / after request handlers. So how does the extension +know the name of the application object? + +Quite simple: you pass it to it. + +There are two recommended ways for an extension to initialize: + +initialization functions: + If your extension is called `helloworld` you might have a function + called ``init_helloworld(app[, extra_args])`` that initalizes the + extension for that application. It could attach before / after + handlers etc. + +classes: + Classes work mostly like initialization functions but can later be + used to further change the behaviour. For an example look at how the + `OAuth extension`_ works: ther is an `OAuth` object that provides + some helper functions like `OAuth.remote_app` to create a reference to + a remote application that uses OAuth. + +What to use depends on what you have in mind. For the SQLite 3 extension +we will need to use the class based approach because we have to use a +controller object that can be used to connect to the database. + +The Extension Code +------------------ + +Here the contents of the `flaskext/sqlite3.py` for copy/paste:: + + from __future__ import absolute_import + import sqlite3 + from flask import g + + class SQLite3(object): + + def __init__(self, app): + self.app = app + self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') + + self.app.before_request(self.before_request) + self.app.after_request(self.after_request) + + def connect(self): + return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) + + def before_request(self): + g.sqlite3_db = self.connect() + + def after_request(self, response): + g.sqlite3_db.close() + return response + +So here what the lines of code do: + +1. the ``__future__`` import is necessary to activate absolute imports. + This is needed because otherwise we could not call our module + `sqlite3.py` and import the top-level `sqlite3` module which actually + implements the connection to SQLite. +2. We create a class for our extension that sets a default configuration + for the SQLite 3 database if it's not there (:meth:`dict.setdefault`) + and connects two functions as before and after request handlers. +3. Then it implements a `connect` function that returns a new database + connection and the two handlers. + +So why did we decide on a class based approach here? Because using that +extension looks something like this:: + + from flask import Flask, g + from flaskext.sqlite3 import SQLite3 + + app = Flask(__name__) + app.config.from_pyfile('the-config.cfg') + db = SQLite(app) + +Either way you can use the database from the views like this:: + + @app.route('/') + def show_all(): + cur = g.sqlite_db.cursor() + cur.execute(...) + +But how would you open a database connection from outside a view function? +This is where the `db` object now comes into play: + +>>> from yourapplication import db +>>> con = db.connect() +>>> cur = con.cursor() + +If you don't need that, you can go with initialization functions. + +Initialization Functions +------------------------ + +Here how the module would look like with initialization functions:: + + from __future__ import absolute_import + import sqlite3 + from flask import g + + def init_sqlite3(app): + app = app + app.config.setdefault('SQLITE3_DATABASE', ':memory:') + + @app.before_request + def before_request(): + g.sqlite3_db = sqlite3.connect(self.app.config['SQLITE3_DATABASE']) + + @app.after_request + def after_request(response): + g.sqlite3_db.close() + return response + +Learn from Others +----------------- + +This documentation only touches the bare minimum for extension +development. If you want to learn more, it's a very good idea to check +out existing extensions on the `Flask Extension Registry`_. If you feel +lost there is still the `mailinglist`_ and the `IRC channel`_ to get some +ideas for nice looking APIs. Especially if you do something nobody before +you did, it might be a very good idea to get some more input. + +Remember: good API design is hard :( + + +.. _Flask Extension Wizard: + http://github.com/mitsuhiko/flask-extension-wizard +.. _OAuth extension: http://packages.python.org/Flask-OAuth/ +.. _mailinglist: http://flask.pocoo.org/mailinglist/ +.. _IRC channel: http://flask.pocoo.org/community/irc/ From 216472b43dbb2c2501ab7f8b3e5ae861a082db3f Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Wed, 2 Jun 2010 03:38:17 +0800 Subject: [PATCH 0245/3747] Fix missing plural in documentation --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 65678a15..99d0d7e5 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -109,7 +109,7 @@ Working with Modules -------------------- For larger applications with more than a dozen views it makes sense to -split the views into module. First let's look at the typical struture of +split the views into modules. First let's look at the typical struture of such an application:: /yourapplication From 33e7f2b990e3889d6aa9cffef4a4c9d7d0923698 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 2 Jun 2010 18:04:49 +0200 Subject: [PATCH 0246/3747] Invoke after_request on exceptions as well. This fixes #59 --- flask.py | 13 +++++++++++++ tests/flask_tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/flask.py b/flask.py index 71c30d84..00d23287 100644 --- a/flask.py +++ b/flask.py @@ -1362,6 +1362,12 @@ class Flask(_PackageBoundObject): Then you still have the original application object around and can continue to call methods on it. + .. versionchanged:: 0.4 + The :meth:`after_request` functions are now called even if an + error handler took over request processing. This ensures that + even if an exception happens database have the chance to + properly close the connection. + :param environ: a WSGI environment :param start_response: a callable accepting a status code, a list of headers and an optional @@ -1376,6 +1382,13 @@ class Flask(_PackageBoundObject): response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) + try: + response = self.process_response(response) + except Exception, e: + self.logger.exception('after_request handler failed ' + 'to postprocess error response. ' + 'Depending on uncertain state?') + return response(environ, start_response) def request_context(self, environ): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 9d1dba6e..3f9588bc 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -16,6 +16,7 @@ import sys import flask import unittest import tempfile +from logging import StreamHandler from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header @@ -240,6 +241,37 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'after' in evts assert rv == 'request|after' + def test_after_request_errors(self): + app = flask.Flask(__name__) + called = [] + @app.after_request + def after_request(response): + called.append(True) + return response + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + assert len(called) == 1 + + def test_after_request_handler_error(self): + error_out = StringIO() + app = flask.Flask(__name__) + app.logger.addHandler(StreamHandler(error_out)) + @app.after_request + def after_request(response): + 1/0 + return response + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + assert 'after_request handler failed' in error_out.getvalue() + def test_error_handling(self): app = flask.Flask(__name__) @app.errorhandler(404) From 937360877bd425543e06decd68a62e086b78bf6b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 2 Jun 2010 18:32:23 +0200 Subject: [PATCH 0247/3747] Documented changes in after_request handlers. --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 83d1af06..901f08b2 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,9 @@ Release date to be announced, codename to be selected. - added the ability to register application wide error handlers from modules. +- :meth:`~flask.Flask.after_request` handlers are now also invoked + if the request dies with an exception and an error handling page + kicks in. Version 0.3.1 ------------- From bc00fd1e83f23f57dd6a765b6a4bab2394584ae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 3 Jun 2010 15:26:07 +0200 Subject: [PATCH 0248/3747] Added support for deferred context cleanup. test_client users can now access the context locals after the actual request if the client is used with a with-block. This fixes #59. --- CHANGES | 3 +++ docs/api.rst | 17 +++++++++++++++++ docs/testing.rst | 24 ++++++++++++++++++++++++ flask.py | 39 ++++++++++++++++++++++++++++++++++++--- tests/flask_tests.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 901f08b2..ca066dad 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,9 @@ Release date to be announced, codename to be selected. - :meth:`~flask.Flask.after_request` handlers are now also invoked if the request dies with an exception and an error handling page kicks in. +- test client has not the ability to preserve the request context + for a little longer. This can also be used to trigger custom + requests that do not pop the request stack for testing. Version 0.3.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index 0fb50551..fc6f68fb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -300,3 +300,20 @@ Useful Internals all the context local objects used in Flask. This is a documented instance and can be used by extensions and application code but the use is discouraged in general. + + .. versionchanged:: 0.4 + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of `DEBUG` mode. By setting + ``'flask._preserve_context'`` to `True` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. diff --git a/docs/testing.rst b/docs/testing.rst index db2b4188..de14413d 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -218,3 +218,27 @@ All the other objects that are context bound can be used the same. If you want to test your application with different configurations and there does not seem to be a good way to do that, consider switching to application factories (see :ref:`app-factories`). + + +Keeping the Context Around +-------------------------- + +.. versionadded:: 0.4 + +Sometimes it can be helpful to trigger a regular request but keep the +context around for a little longer so that additional introspection can +happen. With Flask 0.4 this is possible by using the +:meth:`~flask.Flask.test_client` with a `with` block:: + + app = flask.Flask(__name__) + + with app.test_client() as c: + rv = c.get('/?foo=42') + assert request.args['foo'] == '42' + +If you would just be using the :meth:`~flask.Flask.test_client` without +the `with` block, the `assert` would fail with an error because `request` +is no longer available (because used outside of an actual request). +Keep in mind however that :meth:`~flask.Flask.after_request` functions +are already called at that point so your database connection and +everything involved is probably already closed down. diff --git a/flask.py b/flask.py index 00d23287..f5d94fbe 100644 --- a/flask.py +++ b/flask.py @@ -163,8 +163,10 @@ class _RequestContext(object): def __exit__(self, exc_type, exc_value, tb): # do not pop the request stack if we are in debug mode and an # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. - if tb is None or not self.app.debug: + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + if not self.request.environ.get('flask._preserve_context') and \ + (tb is None or not self.app.debug): self.pop() @@ -1021,9 +1023,40 @@ class Flask(_PackageBoundObject): def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?foo=42') + assert request.args['foo'] == '42' + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. """ from werkzeug import Client - return Client(self, self.response_class, use_cookies=True) + class FlaskClient(Client): + preserve_context = context_preserved = False + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + def __enter__(self): + self.preserve_context = True + return self + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() + return FlaskClient(self, self.response_class, use_cookies=True) def open_session(self, request): """Creates or opens a new session. Default implementation stores all diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3f9588bc..6fbc88a6 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -58,6 +58,7 @@ class ContextTestCase(unittest.TestCase): assert index() == 'Hello World!' with app.test_request_context('/meh'): assert meh() == 'http://localhost/meh' + assert flask._request_ctx_stack.top is None def test_manual_context_binding(self): app = flask.Flask(__name__) @@ -76,6 +77,36 @@ class ContextTestCase(unittest.TestCase): else: assert 0, 'expected runtime error' + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + assert flask.g.value == 42 + assert resp.data == 'Hello World!' + assert resp.status_code == 200 + + resp = c.get('/other') + assert not hasattr(flask.g, 'value') + assert 'Internal Server Error' in resp.data + assert resp.status_code == 500 + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + class BasicFunctionalityTestCase(unittest.TestCase): From 9983e847425621b5e474ae9da6822f1af0b0cc1f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 3 Jun 2010 16:21:23 +0200 Subject: [PATCH 0249/3747] Fixed after_request handlers being called twice in some cases and improved logging system --- CHANGES | 2 ++ flask.py | 57 ++++++++++++++++++++++++++++---------------- tests/flask_tests.py | 16 +++++++++---- 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/CHANGES b/CHANGES index ca066dad..0eb34dc1 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Release date to be announced, codename to be selected. - test client has not the ability to preserve the request context for a little longer. This can also be used to trigger custom requests that do not pop the request stack for testing. +- because the Python standard library caches loggers, the name of + the logger is configurable now to better support unittests. Version 0.3.1 ------------- diff --git a/flask.py b/flask.py index f5d94fbe..8e78bea8 100644 --- a/flask.py +++ b/flask.py @@ -16,6 +16,7 @@ import mimetypes from datetime import datetime, timedelta from itertools import chain +from threading import Lock from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ @@ -50,6 +51,9 @@ try: except (ImportError, AttributeError): pkg_resources = None +# a lock used for logger initialization +_logger_lock = Lock() + class Request(RequestBase): """The request object used by default in flask. Remembers the @@ -839,6 +843,12 @@ class Flask(_PackageBoundObject): #: `USE_X_SENDFILE` configuration key. Defaults to `False`. use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') + #: the name of the logger to use. By default the logger name is the + #: package name passed to the constructor. + #: + #: .. versionadded:: 0.4 + logger_name = ConfigAttribute('LOGGER_NAME') + #: the logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging #: handler does the formatting. @@ -863,7 +873,8 @@ class Flask(_PackageBoundObject): 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), - 'USE_X_SENDFILE': False + 'USE_X_SENDFILE': False, + 'LOGGER_NAME': None }) def __init__(self, import_name): @@ -874,6 +885,10 @@ class Flask(_PackageBoundObject): #: to load a config from files. self.config = Config(self.root_path, self.default_config) + #: prepare the deferred setup of the logger + self._logger = None + self.logger_name = self.import_name + #: a dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. @@ -952,7 +967,7 @@ class Flask(_PackageBoundObject): ) self.jinja_env.filters['tojson'] = _tojson_filter - @cached_property + @property def logger(self): """A :class:`logging.Logger` object for this application. The default configuration is to log to stderr if the application is @@ -965,17 +980,23 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.3 """ - from logging import getLogger, StreamHandler, Formatter, DEBUG - class DebugHandler(StreamHandler): - def emit(x, record): - if self.debug: - StreamHandler.emit(x, record) - handler = DebugHandler() - handler.setLevel(DEBUG) - handler.setFormatter(Formatter(self.debug_log_format)) - logger = getLogger(self.import_name) - logger.addHandler(handler) - return logger + if self._logger and self._logger.name == self.logger_name: + return self._logger + with _logger_lock: + if self._logger and self._logger.name == self.logger_name: + return self._logger + from logging import getLogger, StreamHandler, Formatter, DEBUG + class DebugHandler(StreamHandler): + def emit(x, record): + if self.debug: + StreamHandler.emit(x, record) + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(self.debug_log_format)) + logger = getLogger(self.logger_name) + logger.addHandler(handler) + self._logger = logger + return logger def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for @@ -1412,16 +1433,12 @@ class Flask(_PackageBoundObject): if rv is None: rv = self.dispatch_request() response = self.make_response(rv) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + try: response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - self.logger.exception('after_request handler failed ' - 'to postprocess error response. ' - 'Depending on uncertain state?') - return response(environ, start_response) def request_context(self, environ): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6fbc88a6..eccf68f3 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -288,11 +288,11 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert len(called) == 1 def test_after_request_handler_error(self): - error_out = StringIO() + called = [] app = flask.Flask(__name__) - app.logger.addHandler(StreamHandler(error_out)) @app.after_request def after_request(response): + called.append(True) 1/0 return response @app.route('/') @@ -301,7 +301,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = app.test_client().get('/') assert rv.status_code == 500 assert 'Internal Server Error' in rv.data - assert 'after_request handler failed' in error_out.getvalue() + assert len(called) == 1 def test_error_handling(self): app = flask.Flask(__name__) @@ -707,6 +707,14 @@ class SendfileTestCase(unittest.TestCase): class LoggingTestCase(unittest.TestCase): + def test_logger_cache(self): + app = flask.Flask(__name__) + logger1 = app.logger + assert app.logger is logger1 + assert logger1.name == __name__ + app.logger_name = __name__ + '/test_logger_cache' + assert app.logger is not logger1 + def test_debug_log(self): app = flask.Flask(__name__) app.debug = True @@ -736,9 +744,9 @@ class LoggingTestCase(unittest.TestCase): assert False, 'debug log ate the exception' def test_exception_logging(self): - from logging import StreamHandler out = StringIO() app = flask.Flask(__name__) + app.logger_name = 'flask_tests/test_exception_logging' app.logger.addHandler(StreamHandler(out)) @app.route('/') From 1ffa295d933c7e0cd1ee14c1592c40f72b9f22bb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 3 Jun 2010 16:25:15 +0200 Subject: [PATCH 0250/3747] Added message that setup.py develop is required to build the docs --- docs/conf.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cffb1b7f..6308beee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,12 +43,20 @@ master_doc = 'index' project = u'Flask' copyright = u'2010, Armin Ronacher' -import pkg_resources - # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -release = __import__('pkg_resources').get_distribution('Flask').version +import pkg_resources +try: + release = pkg_resources.get_distribution('Flask').version +except pkg_resources.DistributionNotFound: + print 'To build the documentation, The distribution information of Flask' + print 'Has to be available. Either install the package into your' + print 'development environment or run "setup.py develop" to setup the' + print 'metadata. A virtualenv is recommended!' + sys.exit(1) +del pkg_resources + if 'dev' in release: release = release.split('dev')[0] + 'dev' version = '.'.join(release.split('.')[:2]) From f904ef27d3d1f4e03972c34858787294c74c164f Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 05:23:19 +0800 Subject: [PATCH 0251/3747] Fixing typo in error handling documentation (s/changes are/chances are/) --- docs/errorhandling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index f963f1ab..ae292a1a 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -35,7 +35,7 @@ If the application runs in production mode (which it will do on your server) you won't see any log messages by default. Why that? Flask tries to be a zero-configuration framework and where should it drop the logs for you if there is no configuration. Guessing is not a good idea because -changes are, the place it guessed is not the place where the user has the +chances are, the place it guessed is not the place where the user has the permission to create a logfile. Also, for most small applications nobody will look at the logs anyways. From 12c2cb5e12895a0d16b37b7a9e9a96d69a34c36e Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 07:55:41 +0800 Subject: [PATCH 0252/3747] Typo in docs for Flask.handle_exception --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 8e78bea8..2ca3e5a4 100644 --- a/flask.py +++ b/flask.py @@ -1301,7 +1301,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception occours that is not catched. In debug mode the exception will - be re-raised immediately, otherwise it is logged an the handler + be re-raised immediately, otherwise it is logged and the handler for an 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. From cc7876f97fbc787ac7702453bdc62541f8ea47f6 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 07:59:02 +0800 Subject: [PATCH 0253/3747] Fix in docs for handle_exception: "A" before nouns pronounced with an initial consonant sound --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 2ca3e5a4..32a97cbf 100644 --- a/flask.py +++ b/flask.py @@ -1302,7 +1302,7 @@ class Flask(_PackageBoundObject): """Default exception handling that kicks in when an exception occours that is not catched. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler - for an 500 internal server error is used. If no such handler + for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. .. versionadded: 0.3 From e67903d134039b2452b2a3806e10451491ce768d Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 08:03:37 +0800 Subject: [PATCH 0254/3747] Fixing another article error, in handle_http_exception. --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 32a97cbf..abcd08e5 100644 --- a/flask.py +++ b/flask.py @@ -1287,7 +1287,7 @@ class Flask(_PackageBoundObject): return f def handle_http_exception(self, e): - """Handles an HTTP exception. By default this will invoke the + """Handles a HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. From e7a9df784c9edee7742052aadacc2389bf21230c Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 13:49:04 +0800 Subject: [PATCH 0255/3747] Typo in jQuery pattern docs --- docs/patterns/jquery.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index e9c9f956..397635c7 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -76,7 +76,7 @@ inside a `script` block here where different rules apply. In HTML the `script` tag is declared `CDATA` which means that entities will not be parsed. Everything until ```` is handled as script. This also means that there must never be any ``"|tojson|safe }}`` is rendered as ``"<\/script>"``). From 2fb09d07f29aaacac3afd6b64c997e761a292864 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sat, 5 Jun 2010 15:06:14 +0800 Subject: [PATCH 0256/3747] Another typo in jQuery pattern --- docs/patterns/jquery.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 397635c7..a97f7ff4 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -150,7 +150,7 @@ explanation of the little bit of code above: 1. ``$(function() { ... })`` specifies code that should run once the browser is done loading the basic parts of the page. -2. ``#('selector')`` selects an element and lets you operate on it. +2. ``$('selector')`` selects an element and lets you operate on it. 3. ``element.bind('event', func)`` specifies a function that should run when the user clicked on the element. If that function returns `false`, the default behaviour will not kick in (in this case, navigate From 7dae84f0027945bac29b45b63fdc0f795a5a813a Mon Sep 17 00:00:00 2001 From: Merlin Date: Fri, 4 Jun 2010 01:48:44 +0800 Subject: [PATCH 0257/3747] Enhanced the nginx deployment subsection of the documentation with working examples. 'tricky' was changed to 'different' to offer a more neutral tone. 'some' was changed to 'no' and 'not properly' swapped with 'by default' to explain the difference. Configuration allows for static serving of media alongside a FastCGI application in a clean declarative manner. Tested on nginx 0.7.x and 0.8.x --- docs/deploying/fastcgi.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 7949cca4..a29664e8 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -73,26 +73,27 @@ root. Configuring nginx ----------------- -Installing FastCGI applications on nginx is a bit tricky because by default -some FastCGI parameters are not properly forwarded. +Installing FastCGI applications on nginx is a bit different because by default +no FastCGI parameters are forwarded. -A basic FastCGI configuration for nginx looks like this:: +A basic flask FastCGI configuration for nginx looks like this:: - location /yourapplication/ { + location = /yourapplication { rewrite ^ /yourapplication/ last; } + location /yourapplication { try_files $uri @yourapplication; } + location @yourapplication { include fastcgi_params; - if ($uri ~ ^/yourapplication/(.*)?) { - set $path_url $1; - } - fastcgi_param PATH_INFO $path_url; - fastcgi_param SCRIPT_NAME /yourapplication; + fastcgi_split_path_info ^(/yourapplication)(.*)$; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; } This configuration binds the application to `/yourapplication`. If you want -to have it in the URL root it's a bit easier because you don't have to figure +to have it in the URL root it's a bit simpler because you don't have to figure out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: - location /yourapplication/ { + location / { try_files $uri @yourapplication; } + location @yourapplication { include fastcgi_params; fastcgi_param PATH_INFO $fastcgi_script_name; fastcgi_param SCRIPT_NAME ""; From 88a031ab24b0e7e2a5190f9e48f80f2b21aba1a0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 5 Jun 2010 04:36:04 -0700 Subject: [PATCH 0258/3747] Revert that change, an HTTP is correct from what I know. --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index abcd08e5..32a97cbf 100644 --- a/flask.py +++ b/flask.py @@ -1287,7 +1287,7 @@ class Flask(_PackageBoundObject): return f def handle_http_exception(self, e): - """Handles a HTTP exception. By default this will invoke the + """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. From e3de5e6ca39e024874cb465c9047b65373d73b64 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sat, 5 Jun 2010 21:55:10 +0800 Subject: [PATCH 0259/3747] Typo in extension development docs. --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index ca8a4ea4..b0eff526 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -222,7 +222,7 @@ Either way you can use the database from the views like this:: @app.route('/') def show_all(): - cur = g.sqlite_db.cursor() + cur = g.sqlite3_db.cursor() cur.execute(...) But how would you open a database connection from outside a view function? From 51a5add86d0fa9889b69d51d45d06a2ac4c5df9f Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sun, 6 Jun 2010 00:27:38 +0800 Subject: [PATCH 0260/3747] Fix inconsistent capitalisation and punctuation in attribute docstrings --- flask.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/flask.py b/flask.py index 32a97cbf..81cd582f 100644 --- a/flask.py +++ b/flask.py @@ -448,11 +448,11 @@ else: class _PackageBoundObject(object): def __init__(self, import_name): - #: the name of the package or module. Do not change this once + #: The name of the package or module. Do not change this once #: it was set by the constructor. self.import_name = import_name - #: where is the app root located? + #: Where is the app root located? self.root_path = _get_package_path(self.import_name) def open_resource(self, resource): @@ -788,20 +788,20 @@ class Flask(_PackageBoundObject): app = Flask(__name__) """ - #: the class that is used for request objects. See :class:`~flask.Request` + #: The class that is used for request objects. See :class:`~flask.Request` #: for more information. request_class = Request - #: the class that is used for response objects. See + #: The class that is used for response objects. See #: :class:`~flask.Response` for more information. response_class = Response - #: path for the static files. If you don't want to use static files + #: Path for the static files. If you don't want to use static files #: you can set this value to `None` in which case no URL rule is added #: and the development server will no longer serve any static files. static_path = '/static' - #: the debug flag. Set this to `True` to enable debugging of the + #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload #: the application if changes in the code are detected. @@ -810,7 +810,7 @@ class Flask(_PackageBoundObject): #: configuration key. Defaults to `False`. debug = ConfigAttribute('DEBUG') - #: if a secret key is set, cryptographic components can use this to + #: If a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. #: @@ -818,7 +818,7 @@ class Flask(_PackageBoundObject): #: `SECRET_KEY` configuration key. Defaults to `None`. secret_key = ConfigAttribute('SECRET_KEY') - #: The secure cookie uses this for the name of the session cookie + #: The secure cookie uses this for the name of the session cookie. #: #: This attribute can also be configured from the config with the #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` @@ -843,13 +843,13 @@ class Flask(_PackageBoundObject): #: `USE_X_SENDFILE` configuration key. Defaults to `False`. use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') - #: the name of the logger to use. By default the logger name is the + #: The name of the logger to use. By default the logger name is the #: package name passed to the constructor. #: #: .. versionadded:: 0.4 logger_name = ConfigAttribute('LOGGER_NAME') - #: the logging format used for the debug logger. This is only used when + #: The logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging #: handler does the formatting. #: @@ -861,13 +861,13 @@ class Flask(_PackageBoundObject): '-' * 80 ) - #: options that are passed directly to the Jinja2 environment + #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( autoescape=True, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) - #: default configuration parameters + #: Default configuration parameters. default_config = ImmutableDict({ 'DEBUG': False, 'SECRET_KEY': None, @@ -880,29 +880,29 @@ class Flask(_PackageBoundObject): def __init__(self, import_name): _PackageBoundObject.__init__(self, import_name) - #: the configuration dictionary as :class:`Config`. This behaves + #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods #: to load a config from files. self.config = Config(self.root_path, self.default_config) - #: prepare the deferred setup of the logger + #: Prepare the deferred setup of the logger. self._logger = None self.logger_name = self.import_name - #: a dictionary of all view functions registered. The keys will + #: A dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. #: to register a view function, use the :meth:`route` decorator. self.view_functions = {} - #: a dictionary of all registered error handlers. The key is + #: A dictionary of all registered error handlers. The key is #: be the error code as integer, the value the function that #: should handle that error. #: To register a error handler, use the :meth:`errorhandler` #: decorator. self.error_handlers = {} - #: a dictionary with lists of functions that should be called at the + #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of #: the module this function is active for, `None` for all requests. #: This can for example be used to open database connections or @@ -910,7 +910,7 @@ class Flask(_PackageBoundObject): #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} - #: a dictionary with lists of functions that should be called after + #: A dictionary with lists of functions that should be called after #: each request. The key of the dictionary is the name of the module #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the @@ -918,7 +918,7 @@ class Flask(_PackageBoundObject): #: :meth:`before_request` decorator. self.after_request_funcs = {} - #: a dictionary with list of functions that are called without argument + #: A dictionary with list of functions that are called without argument #: to populate the template context. They key of the dictionary is the #: name of the module this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is @@ -928,7 +928,7 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } - #: the :class:`~werkzeug.routing.Map` for this instance. You can use + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: #: @@ -956,7 +956,7 @@ class Flask(_PackageBoundObject): self.static_path: target }) - #: the Jinja2 environment. It is created from the + #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned #: by the :meth:`create_jinja_loader` function. self.jinja_env = Environment(loader=self.create_jinja_loader(), From 06ed3e1e8888b73794876d39a53382eb45a54fe2 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Sun, 6 Jun 2010 00:30:11 +0800 Subject: [PATCH 0261/3747] Misleading templates path example in docs for open_resource --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 81cd582f..ca69a056 100644 --- a/flask.py +++ b/flask.py @@ -463,7 +463,7 @@ class _PackageBoundObject(object): /schemal.sql /static /style.css - /template + /templates /layout.html /index.html From f5fb4576577cbedcedba9eb16d9fdace18c9292c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 7 Jun 2010 00:56:02 +0200 Subject: [PATCH 0262/3747] Added TESTING flag. This fixes #58. --- CHANGES | 1 + docs/config.rst | 1 + flask.py | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 0eb34dc1..1dca6ee6 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Release date to be announced, codename to be selected. requests that do not pop the request stack for testing. - because the Python standard library caches loggers, the name of the logger is configurable now to better support unittests. +- added `TESTING` switch that can activate unittesting helpers. Version 0.3.1 ------------- diff --git a/docs/config.rst b/docs/config.rst index de79cabc..e057ba4d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -53,6 +53,7 @@ The following configuration values are used internally by Flask: =============================== ========================================= ``DEBUG`` enable/disable debug mode +``TESTING`` enable/disable testing mode ``SECRET_KEY`` the secret key ``SESSION_COOKIE_NAME`` the name of the session cookie ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as diff --git a/flask.py b/flask.py index ca69a056..c4656cdb 100644 --- a/flask.py +++ b/flask.py @@ -810,6 +810,15 @@ class Flask(_PackageBoundObject): #: configuration key. Defaults to `False`. debug = ConfigAttribute('DEBUG') + #: The testing flask. Set this to `True` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate unittest helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: This attribute can also be configured from the config with the + #: `TESTING` configuration key. Defaults to `False`. + testing = ConfigAttribute('TESTING') + #: If a secret key is set, cryptographic components can use this to #: sign cookies and other things. Set this to a complex random value #: when you want to use the secure cookie for instance. @@ -870,6 +879,7 @@ class Flask(_PackageBoundObject): #: Default configuration parameters. default_config = ImmutableDict({ 'DEBUG': False, + 'TESTING': False, 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), From ca0aa9533f7c3a925f1115581872793bdf34bb34 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 7 Jun 2010 02:18:31 +0200 Subject: [PATCH 0263/3747] Added an HTML FAQ document, first draft. --- docs/contents.rst.inc | 1 + docs/htmlfaq.rst | 162 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 docs/htmlfaq.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 9a3ff5a8..2422d12e 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -40,6 +40,7 @@ Design notes, legal information and changelog are here for the interested. :maxdepth: 2 design + htmlfaq extensiondev license upgrading diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst new file mode 100644 index 00000000..b92beb77 --- /dev/null +++ b/docs/htmlfaq.rst @@ -0,0 +1,162 @@ +HTML/XHTML FAQ +============== + +The Flask documentation and example applications are using HTML5. You +will notice that in many situations when end tags are optional they are +not used to keep the HTML cleaner and also faster to load. Because there +is a lot of confusion about HTML and XHTML out there this document tries +to answer some of them. + + +History on XHTML +---------------- + +For a while it looked like HTML was about to be replaced by XHTML. +However barely any websites on the internet are actually real XHTML (which +means XHTML processed with XML rules). There are a couple of reasons why +this is the case. It mostly has to do with Internet Explorer which does +not accept the XHTML mimetype to switch the browser into XML mode. +However this is really easy to bypass but barely anyone does that. This +probably has to do with the fact that XHTML is really painful. + +Why is it painful? XML has very strict errorhandling. On a parsing error +the browser is supposed to show the user an ugly error message. Most of +the (X)HTML generation on the web is based on non-XML template engines +(such as Jinja, the one used in Flask) which do not protect you from +accidentally creating invalid HTML. There are XML based template engines +but they usually come with a larger runtime overhead and are not as +straightforward to use because they have to obey XML rules. + +Now the majority of users assumed they were using XHTML though. The +reasons for that is that they sticked an XHTML doctype on top of the +document and self-closed all necessary tags (``
    `` becomes ``
    `` or +``

    `` in XHTML). However even if the document properly validates +as XHTML there are still other things to keep in mind. + +XHTML also changes the way you work with JavaScript because you now have +to use the namespaced DOM interface with the XHTML namespace to query for +HTML elements. + +History of HTML5 +---------------- + +HTML5 was started in 2004 under the name Web Applications 1.0 by the +WHATWG (Apple, Mozilla, Opera) and the idea was to write a new and +improved specification of HTML based on actual browser behaviour instead +of behaviour that exists on the paper but could not be implemented +because of backwards compatibility with the already existing web. + +For example in theory HTML4 ``Hello`` but because existing websites are using +pseudo-XHTML which uses the Slash in different ways, this could not be +implemented properly. + +In 2007 the specification was adopted as the basis of a new HTML +specification under the umbrella of the W3C. Currently it looks like +XHTML is losing traction, the XHTML 2 working group was disbanded and +HTML5 is being implemented by all major browser vendors. + +HTML versus XHTML +----------------- + +The following table gives you a quick overview of features available in +HTML 4.01, XHTML 1.1 and HTML5 (we are not looking at XHTML 1.0 here which +was superceeded by XHTML 1.1 or XHTML5 which is barely supported currently): + ++-----------------------------------------+----------+----------+----------+ +| | HTML4.01 | XHTML1.1 | HTML5 | ++=========================================+==========+==========+==========+ +| ``value`` | |Y| [1]_ | |N| | |N| | ++-----------------------------------------+----------+----------+----------+ +| ``
    `` supported | |N| | |Y| | |Y| [2]_ | ++-----------------------------------------+----------+----------+----------+ +| `` + + + +If you know a bit of JavaScript internals you might know that it's +possible to patch constructors and register callbacks for setters. An +attacker can use this (like above) to get all the data you exported in +your JSON file. The browser will totally ignore the ``application/json`` +mimetype if ``text/javascript`` is defined as content type in the script +tag and evaluate that as JavaScript. Because toplevel array elements are +allowed (albeit useless) and we hooked in our own constructor, after that +page loaded the data from the JSON response is in the `captured` array. + +Because it is a syntax error in JavaScript to have an object literal +(``{...}``) toplevel an attacker could not just do a request to an +external URL with the script tag to load up the data. So what Flask does +is only allowing objects as toplevel elements when using +:func:`~flask.jsonify`. Make sure to do the same when using an ordinary +JSON generate function. diff --git a/flask.py b/flask.py index c4656cdb..227bbf1e 100644 --- a/flask.py +++ b/flask.py @@ -300,7 +300,9 @@ def jsonify(*args, **kwargs): "id": 42 } - This requires Python 2.6 or an installed version of simplejson. + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. .. versionadded:: 0.2 """ From 52f38bbf8bee2f7b5aa4fe708dc304cb590d3a0b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 10 Jun 2010 23:20:16 +0200 Subject: [PATCH 0274/3747] Fixed a broken sentence --- docs/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index db3d3c74..18b976ab 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -67,8 +67,8 @@ recursive structures that could cause problems and the only thing that could possibly break are very large responses that can cause some kind of denial of service at the receivers side. -However there is a catch. Due to how browsers the CSRF issue comes up -with JSON unfortunately. Fortunately there is also a weird part of the +However there is a catch. Due to how browsers work the CSRF issue comes +up with JSON unfortunately. Fortunately there is also a weird part of the JavaScript specification that can be used to solve that problem easily and Flask is kinda doing that for you by preventing you from doing dangerous stuff. Unfortunately that protection is only there for From 20c2e53893c202a4555bd7390672ee948fa6b1e0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 11 Jun 2010 01:37:35 +0200 Subject: [PATCH 0275/3747] Alcoholified placeholders --- docs/testing.rst | 4 ++-- flask.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index de14413d..f9785b18 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -233,8 +233,8 @@ happen. With Flask 0.4 this is possible by using the app = flask.Flask(__name__) with app.test_client() as c: - rv = c.get('/?foo=42') - assert request.args['foo'] == '42' + rv = c.get('/?tequila=42') + assert request.args['tequila'] == '42' If you would just be using the :meth:`~flask.Flask.test_client` without the `with` block, the `assert` would fail with an error because `request` diff --git a/flask.py b/flask.py index 227bbf1e..93b39e05 100644 --- a/flask.py +++ b/flask.py @@ -213,7 +213,7 @@ def url_for(endpoint, **values): def get_template_attribute(template_name, attribute): """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a - template named `_foo.html` with the following contents: + template named `_cider.html` with the following contents: .. sourcecode:: html+jinja @@ -221,7 +221,7 @@ def get_template_attribute(template_name, attribute): You can access this from Python code like this:: - hello = get_template_attribute('_foo.html', 'hello') + hello = get_template_attribute('_cider.html', 'hello') return hello('World') .. versionadded:: 0.2 @@ -1062,8 +1062,8 @@ class Flask(_PackageBoundObject): you want to access the context locals for testing:: with app.test_client() as c: - rv = c.get('/?foo=42') - assert request.args['foo'] == '42' + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' .. versionchanged:: 0.4 added support for `with` block usage for the client. From ea72e272a677365a60f1c412f91ebfac58ba13bb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Jun 2010 18:11:52 +0200 Subject: [PATCH 0276/3747] Fixed a broken example --- docs/errorhandling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index ae292a1a..cc7437a6 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -151,7 +151,7 @@ File logging from logging import Formatter file_handler.setFormatter(Formatter( - '%(astime)s %(levelname)s: %(message)s ' + '%(asctime)s %(levelname)s: %(message)s ' '[in %(pathname)s:%(lineno)d]' )) From f66c252e47c63d7961b8d2c00acdf5aed11eb851 Mon Sep 17 00:00:00 2001 From: Andre Wobst Date: Tue, 15 Jun 2010 07:00:17 +0800 Subject: [PATCH 0277/3747] typo --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index fd9bc689..db5b00a3 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -186,7 +186,7 @@ different module (say `frontend`). This would look like this:: def index(): return "I'm the frontend index" -Now let's say we only want to redirect to a different module in the same +Now let's say we only want to redirect to a different function in the same module. Then we can either use the full qualified endpoint name like we did in the example above, or we just use the function name:: From 387be22f26b8065d29e311723bb8a50a74248534 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 17 Jun 2010 19:01:32 +0200 Subject: [PATCH 0278/3747] Added unicode chapter to the docs. This fixes #67 --- docs/contents.rst.inc | 1 + docs/unicode.rst | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 docs/unicode.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 3dbe28aa..bb32fa4d 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -42,6 +42,7 @@ Design notes, legal information and changelog are here for the interested. design htmlfaq security + unicode extensiondev license upgrading diff --git a/docs/unicode.rst b/docs/unicode.rst new file mode 100644 index 00000000..d439797e --- /dev/null +++ b/docs/unicode.rst @@ -0,0 +1,81 @@ +Unicode in Flask +================ + +Flask like Jinja2 and Werkzeug is totally unicode based when it comes to +text. Not only these libraries, also the majority of web related Python +libraries that deal with text. If you don't know unicode so far, you +should probably read `The Absolute Minimum Every Software Developer +Absolutely, Positively Must Know About Unicode and Character Sets +`_. This part of the +documentation just tries to cover the very basics so that you have a +pleasent experience with unicode related things. + +Automatic Conversion +-------------------- + +Flask has a few assumptions about your application (which you can change +of course) that give you basic and painless unicode support: + +- the encoding for text on your website is UTF-8 +- internally you will always use unicode exclusively for text except + for literal strings with only ASCII character points. +- encoding and decoding happens whenever you are talking over a protocol + that requires bytes to be transmitted. + +So what does this mean to you? + +HTTP is based on bytes. Not only the protocol, also the system used to +address documents on servers (so called URIs or URLs). However HTML which +is usually transmitted on top of HTTP supports a large variety of +character sets and which ones are used, are transmitted in an HTTP header. +To not make this too complex Flask just assumes that if you are sending +unicode out you want it to be UTF-8 encoded. Flask will do the encoding +and setting of the appropriate headers for you. + +The same is true if you are talking to databases with the help of +SQLAlchemy or a similar ORM system. Some databases have a protocol that +already transmits unicode and if they do not, SQLAlchemy or your other ORM +should take care of that. + +The Golden Rule +--------------- + +So the rule of thumb: if you are not dealing with binary data, work with +unicode. What does working with unicode in Python 2.x mean? + +- as long as you are using ASCII charpoints only (basically numbers, + some special characters of latin letters without umlauts or anything + fancy) you can use regular string literals (``'Hello World'``). +- if you need anything else than ASCII in a string you have to mark + this string as unicode string by prefixing it with a lowercase `u`. + (like ``u'Hänsel und Gretel'``) +- if you are using non-unicode characters in your Python files you have + to tell Python which encoding your file uses. Again, I recommend + UTF-8 for this purpose. To tell the interpreter yuor encoding you can + put the ``# -*- coding: utf-8 -*-`` into the first or second line of + your Python source file. + +Encoding and Decoding Yourself +------------------------------ + +If you are talking with a filesystem or something that is not really based +on unicode you will have to ensure that you decode properly when working +with unicode interface. So for example if you want to load a file on the +filesystem and embedd it into a Jinja2 template you will have to decode it +form the encoding of that file. Here the old problem that textfiles do +not specify their encoding comes into play. So do yourself a favour and +limit yourself to UTF-8 for textfiles as well. + +Anyways. To load such a file with unicode you can use the builtin +:meth:`str.decode` method:: + + def read_file(filename, charset='utf-8'): + with open(filename, 'r') as f: + return f.read().decode(charset) + +To go from unicode into a specific charset such as UTF-8 you can use the +:meth:`unicode.encode` method:: + + def write_file(filename, contents, charset='utf-8'): + with open(filename, 'w') as f: + f.write(contents.encode(charset)) From 03c4bb4abc3bd8120ba3e5ea493447be2ad57711 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 18 Jun 2010 03:16:35 +0800 Subject: [PATCH 0279/3747] Typo fix. --- docs/unicode.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/unicode.rst b/docs/unicode.rst index d439797e..e9259d12 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -51,7 +51,7 @@ unicode. What does working with unicode in Python 2.x mean? (like ``u'Hänsel und Gretel'``) - if you are using non-unicode characters in your Python files you have to tell Python which encoding your file uses. Again, I recommend - UTF-8 for this purpose. To tell the interpreter yuor encoding you can + UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of your Python source file. From 7083b35e6af836eeeca661f5a1ba9896bc60ebb3 Mon Sep 17 00:00:00 2001 From: florentx Date: Fri, 18 Jun 2010 09:59:20 +0200 Subject: [PATCH 0280/3747] Typo. --- docs/security.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index 18b976ab..05c9a62c 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -61,11 +61,11 @@ the form validation framework which does not exist in Flask. JSON Security ------------- -JSON itself is a high-level serilization format, so there is barely +JSON itself is a high-level serialization format, so there is barely anything that could cause security problems, right? You can't declare recursive structures that could cause problems and the only thing that could possibly break are very large responses that can cause some kind of -denial of service at the receivers side. +denial of service at the receiver's side. However there is a catch. Due to how browsers work the CSRF issue comes up with JSON unfortunately. Fortunately there is also a weird part of the From d44b12774879cba83bf33c8ed7733157ab84d778 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Jun 2010 16:53:38 +0200 Subject: [PATCH 0281/3747] Use a custom logger subclass that uses DEBUG level if in debug mode --- flask.py | 12 ++++++++---- tests/flask_tests.py | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flask.py b/flask.py index 93b39e05..ea25cdae 100644 --- a/flask.py +++ b/flask.py @@ -867,7 +867,7 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.3 debug_log_format = ( '-' * 80 + '\n' + - '%(levelname)s in %(module)s, %(pathname)s:%(lineno)d]:\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + '%(message)s\n' + '-' * 80 ) @@ -997,15 +997,19 @@ class Flask(_PackageBoundObject): with _logger_lock: if self._logger and self._logger.name == self.logger_name: return self._logger - from logging import getLogger, StreamHandler, Formatter, DEBUG + from logging import getLogger, StreamHandler, Formatter, \ + Logger, DEBUG + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if self.debug else Logger.getEffectiveLevel(x) class DebugHandler(StreamHandler): def emit(x, record): - if self.debug: - StreamHandler.emit(x, record) + StreamHandler.emit(x, record) if self.debug else None handler = DebugHandler() handler.setLevel(DEBUG) handler.setFormatter(Formatter(self.debug_log_format)) logger = getLogger(self.logger_name) + logger.__class__ = DebugLogger logger.addHandler(handler) self._logger = logger return logger diff --git a/tests/flask_tests.py b/tests/flask_tests.py index eccf68f3..f0f15a7b 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -718,9 +718,11 @@ class LoggingTestCase(unittest.TestCase): def test_debug_log(self): app = flask.Flask(__name__) app.debug = True + @app.route('/') def index(): app.logger.warning('the standard library is dead') + app.logger.debug('this is a debug statement') return '' @app.route('/exc') @@ -731,9 +733,10 @@ class LoggingTestCase(unittest.TestCase): with catch_stderr() as err: rv = c.get('/') out = err.getvalue() - assert 'WARNING in flask_tests,' in out + assert 'WARNING in flask_tests [' in out assert 'flask_tests.py' in out assert 'the standard library is dead' in out + assert 'this is a debug statement' in out with catch_stderr() as err: try: From fe35105bec80bbb7cbba1b1d6414612fe85873cd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Jun 2010 17:03:31 +0200 Subject: [PATCH 0282/3747] Updated design decisions. This fixes #63 --- CHANGES | 1 + docs/design.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGES b/CHANGES index 1dca6ee6..5e0956e7 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,7 @@ Release date to be announced, codename to be selected. - because the Python standard library caches loggers, the name of the logger is configurable now to better support unittests. - added `TESTING` switch that can activate unittesting helpers. +- the logger switches to `DEBUG` mode now if debug is enabled. Version 0.3.1 ------------- diff --git a/docs/design.rst b/docs/design.rst index 6a4674ba..20c57a1f 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -73,6 +73,10 @@ want to apply a WSGI middleware, just wrap it and you're done (though there are better ways to do that so that you do not lose the reference to the application object :meth:`~flask.Flask.wsgi_app`). +Furthermore this design makes it possible to use a factory function to +create the application which is very helpful for unittesting and similar +things (:ref:`app-factories`). + One Template Engine ------------------- @@ -145,3 +149,21 @@ quick and easy to write a traditional web application. Also see the :ref:`becomingbig` section of the documentation for some inspiration for larger applications based on Flask. + + +What Flask is, What Flask is Not +-------------------------------- + +Flask will never have a database layer. It will not have a form library +or anything else in that direction. Flask itself just bridges to Werkzeug +to implement a proper WSGI application and to Jinja2 to handle templating. +It also binds to a few common standard library packages such as logging. +Everything else is up for extensions. + +Why is this the case? Because people have different preferences and +requirements and Flask could not meet those if it would force any of this +into the core. The majority of web applications will need a template +engine in some sort. However not every application needs a SQL database. + +The idea of Flask is to build a good foundation for all applications. +Everything else is up to you or extensions. From 7698c3fd14692aeb947d22d0822df061e4312b03 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Jun 2010 17:10:31 +0200 Subject: [PATCH 0283/3747] This will become 0.4 --- CHANGES | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 5e0956e7..ed36526c 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.4 ----------- -Release date to be announced, codename to be selected. +Released on June 18th 2010, codename Rakia - added the ability to register application wide error handlers from modules. @@ -24,7 +24,7 @@ Release date to be announced, codename to be selected. Version 0.3.1 ------------- -Bugfix release, released May 28th +Bugfix release, released May 28th 2010 - fixed a error reporting bug with :meth:`flask.Config.from_envvar` - removed some unused code from flask @@ -35,7 +35,7 @@ Bugfix release, released May 28th Version 0.3 ----------- -Released on May 28th, codename Schnaps +Released on May 28th 2010, codename Schnaps - added support for categories for flashed messages. - the application now configures a :class:`logging.Handler` and will @@ -51,7 +51,7 @@ Released on May 28th, codename Schnaps Version 0.2 ----------- -Released on May 12th, codename Jägermeister +Released on May 12th 2010, codename Jägermeister - various bugfixes - integrated JSON support From 1592c53a664c82d9badac81fa0104af226cce5a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Jun 2010 17:13:32 +0200 Subject: [PATCH 0284/3747] Fixed a MANIFEST issue --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 709e1791..166311d4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,8 @@ recursive-include examples * recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs *.pyo +recursive-exclude tests *.pyc +recursive-exclude tests *.pyo recursive-exclude examples *.pyc recursive-exclude examples *.pyo prune docs/_build From 528ae04be024ea27881f4c45e23590c011a8ea66 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Jun 2010 17:14:43 +0200 Subject: [PATCH 0285/3747] HEAD is 0.5-dev --- CHANGES | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ed36526c..d77a1f50 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.5 +----------- + +Codename to be decided, release date to be announced. + Version 0.4 ----------- diff --git a/setup.py b/setup.py index 96fc77ba..404fad32 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.4', + version='0.5', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From b3f342e1816e86be3c367c42a6dd7a9c2bd6dcc9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 19 Jun 2010 13:49:01 +0200 Subject: [PATCH 0286/3747] Updated license, relicensed logo under debian-ish license --- artwork/LICENSE | 20 ++++++++++++++++++++ docs/LICENSE | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 artwork/LICENSE create mode 100644 docs/LICENSE diff --git a/artwork/LICENSE b/artwork/LICENSE new file mode 100644 index 00000000..c6df416c --- /dev/null +++ b/artwork/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +This logo or a modified version may be used by anyone to refer to the +Flask project, but does not indicate endorsement by the project. + +Redistribution and use in source (the SVG file) and binary forms (rendered +PNG files etc.) of the image, with or without modification, are permitted +provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright + notice and this list of conditions. + +* The names of the contributors to the Flask software (see AUTHORS) may + not be used to endorse or promote products derived from this software + without specific prior written permission. + +Note: we would appreciate that you make the image a link to +http://flask.pocoo.org/ if you use it on a web page. diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 00000000..af19e1a7 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS +for more details. + +Some rights reserved. + +Redistribution and use in source (reStructuredText) and 'compiled' forms (HTML, +PDF, PostScript, RTF and so forth) with or without modification, are permitted +provided that the following conditions are met: + +* Redistributions of source code (reStructuredText) must retain the above + copyright notice, this list of conditions and the following disclaimer as the + first lines of this file unmodified. + +* Redistributions in compiled form (converted to HTML, PDF, PostScript, RTF + and so forth) must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + +THIS DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 7503fc81752d4917fc4a9da808ee6bc37f474ff1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 19 Jun 2010 17:17:50 +0200 Subject: [PATCH 0287/3747] Added upgrading notes --- LICENSE | 29 ++++++++++++++-------------- docs/contents.rst.inc | 2 +- docs/license.rst | 45 ++++++++++++++++++++++++++++++++++--------- docs/upgrading.rst | 13 +++++++++++++ 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/LICENSE b/LICENSE index 6a8df19e..5d269389 100644 --- a/LICENSE +++ b/LICENSE @@ -3,9 +3,9 @@ for more details. Some rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +Redistribution and use in source and binary forms of the software as well +as documentation, with or without modification, are permitted provided +that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. @@ -19,14 +19,15 @@ met: promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index bb32fa4d..f0c21010 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -44,6 +44,6 @@ Design notes, legal information and changelog are here for the interested. security unicode extensiondev - license upgrading changelog + license diff --git a/docs/license.rst b/docs/license.rst index 918a75b1..62e5c75e 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -1,21 +1,48 @@ License ======= -Flask is licensed under a three clause `BSD License`_. It basically -means: do whatever you want with it as long as the copyright in Flask -sticks around, the conditions are not modified and the disclaimer is -present. Furthermore you must not use the names of the authors to promote -derivates of the software without written consent. +Flask is licensed under a three clause BSD License. It basically means: +do whatever you want with it as long as the copyright in Flask sticks +around, the conditions are not modified and the disclaimer is present. +Furthermore you must not use the names of the authors to promote derivates +of the software without written consent. -.. _BSD License: - http://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22New_BSD_License.22.29 +The full license text can be found below (:ref:`flask-license`). For the +documentation and artwork different licenses apply. + +.. _authors: Authors ------- .. include:: ../AUTHORS -License Text ------------- +General License Definitions +--------------------------- + +The following section contains the full license texts for Flask and the +documentation. + +- "AUTHORS" hereby refers to all the authors listed in the + :ref:`authors` section. + +- The ":ref:`flask-license`" applies to all the sourcecode shipped as + part of Flask (Flask itself as well as the examples and the unittests) + as well as documentation. + +- The ":ref:`artwork-license`" applies to the project's Horn-Logo. + +.. _flask-license: + +Flask License +------------- .. include:: ../LICENSE + + +.. _artwork-license: + +Flask Artwork License +--------------------- + +.. include:: ../artwork/LICENSE diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f06523e9..20604c8c 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -14,6 +14,19 @@ This section of the documentation enumerates all the changes in Flask from release to release and how you can change your code to have a painless updating experience. +If you want to use the `easy_install` command to upgrade your Flask +installation, make sure to pass it the ``-U`` parameter:: + + $ easy_install -U Flask + +Version 0.4 +----------- + +For application developers there are no changes that require changes in +your code. In case you are developing on a Flask extension however, and +that extension has a unittest-mode you might want to link the activation +of that mode to the new ``TESTING`` flag. + Version 0.3 ----------- From c68cf834330f94e3018d660a6fc967c16e374762 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 22 Jun 2010 12:06:41 -0700 Subject: [PATCH 0288/3747] Workaround for zip setups for appengine. --- flask.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask.py b/flask.py index ea25cdae..a769692e 100644 --- a/flask.py +++ b/flask.py @@ -15,6 +15,9 @@ import sys import mimetypes from datetime import datetime, timedelta +# this is a workaround for appengine. Do not remove this import +import werkzeug + from itertools import chain from threading import Lock from jinja2 import Environment, PackageLoader, FileSystemLoader From cf29877075a21d5fc4c08780f09bfa9865ad1586 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 12:04:26 +0800 Subject: [PATCH 0289/3747] docs/index.rst: Make alt text more informative. --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index e8ea5c33..16070051 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ Welcome to Flask ================ .. image:: _static/logo-full.png - :alt: The Flask Logo with Subtitle + :alt: Flask: web development, one drop at a time :class: floatingflask Welcome to Flask's documentation. This documentation is divided in From 14fde0779851866ee00a12b4353f1091d3999428 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 12:14:45 +0800 Subject: [PATCH 0290/3747] Copy edited the front page of the documentation. --- docs/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 16070051..c4ded1fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,17 +7,17 @@ Welcome to Flask :alt: Flask: web development, one drop at a time :class: floatingflask -Welcome to Flask's documentation. This documentation is divided in -different parts. I would suggest to get started with the -:ref:`installation` and then heading over to the :ref:`quickstart`. +Welcome to Flask's documentation. This documentation is divided into +different parts. I recommend that you get started with +:ref:`installation` and then head over to the :ref:`quickstart`. Besides the quickstart there is also a more detailed :ref:`tutorial` that shows how to create a complete (albeit small) application with Flask. If -you rather want to dive into all the internal parts of Flask, check out +you'd rather dive into the internals of Flask, check out the :ref:`api` documentation. Common patterns are described in the :ref:`patterns` section. -Flask also depends on two external libraries: the `Jinja2`_ template -engine and the `Werkzeug`_ WSGI toolkit. both of which are not documented +Flask depends on two external libraries: the `Jinja2`_ template +engine and the `Werkzeug`_ WSGI toolkit. These libraries are not documented here. If you want to dive into their documentation check out the following links: From f3dd3da59e269cdf50839d9b0e86fa2849516483 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 13:27:59 +0800 Subject: [PATCH 0291/3747] Copy edited and partially rewrote the foreword. --- docs/contents.rst.inc | 6 +-- docs/foreword.rst | 98 +++++++++++++++++++++---------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index f0c21010..e924202e 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -1,9 +1,9 @@ User's Guide ------------ -This part of the documentation is written text and should give you an idea -how to work with Flask. It's a series of step-by-step instructions for -web development. +This part of the documentation, which is mostly prose, begins with some +background information about Flask, then focuses on step-by-step +instructions for web development with Flask. .. toctree:: :maxdepth: 2 diff --git a/docs/foreword.rst b/docs/foreword.rst index fe466dab..de6b4980 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -2,90 +2,90 @@ Foreword ======== Read this before you get started with Flask. This hopefully answers some -questions about the intention of the project, what it aims at and when you +questions about the purpose and goals of the project, and when you should or should not be using it. -What does Micro Mean? ---------------------- +What does "micro" mean? +----------------------- -The micro in microframework for me means on the one hand being small in -size and complexity but on the other hand also that the complexity of the -applications that are written with these frameworks do not exceed a -certain size. A microframework like Flask sacrifices a few things in -order to be approachable and to be as concise as possible. +To me, the "micro" in microframework refers not only to the simplicity and +small size of the framework, but also to the typically limited complexity +and size of applications that are written with the framework. To be +approachable and concise, a microframework sacrifices a few features that +may be necessary in larger or more complex applications. -For example Flask uses thread local objects internally so that you don't +For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in order to stay threadsafe. While this is a really easy approach and saves you a lot of time, it also does not scale well to large applications. -It's especially painful for more complex unittests and when you suddenly -have to deal with code being executed outside of the context of a request -(for example if you have cronjobs). +It's especially painful for more complex unittests, and when you suddenly +have to deal with code being executed outside of the context of a request, +such as in cron jobs. -Flask provides some tools to deal with the downsides of this approach but -the core problem of this approach obviously stays. It is also based on -convention over configuration which means that a lot of things are -preconfigured in Flask and will work well for smaller applications but not -so much for larger ones (where and how it looks for templates, static -files etc.) +Flask provides some tools to deal with the downsides of this approach, but +the core problem remains. Flask is also based on convention over +configuration, which means that many things are preconfigured and will +work well for smaller applications but not so well for larger ones. For +example, by convention, templates and static files are in subdirectories +within the Python source tree of the application. -But don't worry if your application suddenly grows larger than it was -initially and you're afraid Flask might not grow with it. Even with -larger frameworks you sooner or later will find out that you need +But don't worry if your application suddenly grows larger +and you're afraid Flask might not grow with it. Even with +larger frameworks, you'll eventually discover that you need something the framework just cannot do for you without modification. If you are ever in that situation, check out the :ref:`becomingbig` chapter. -A Framework and An Example +A Framework and an Example -------------------------- -Flask is not only a microframework, it is also an example. Based on +Flask is not only a microframework; it is also an example. Based on Flask, there will be a series of blog posts that explain how to create a framework. Flask itself is just one way to implement a framework on top -of existing libraries. Unlike many other microframeworks Flask does not -try to implement anything on its own, it reuses existing code. +of existing libraries. Unlike many other microframeworks, Flask does not +try to implement everything on its own; it reuses existing code. Web Development is Dangerous ---------------------------- -I'm not even joking. Well, maybe a little. If you write a web -application you are probably allowing users to register and leave their +I'm not joking. Well, maybe a little. If you write a web +application, you are probably allowing users to register and leave their data on your server. The users are entrusting you with data. And even if you are the only user that might leave data in your application, you still -want that data to be stored in a secure manner. +want that data to be stored securely. -Unfortunately there are many ways security of a web application can be +Unfortunately, there are many ways the security of a web application can be compromised. Flask protects you against one of the most common security -problems of modern web applications: cross site scripting (XSS). Unless -you deliberately mark insecure HTML as secure Flask (and the underlying -Jinja2 template engine) have you covered. But there are many more ways to +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to cause security problems. -Whenever something is dangerous where you have to watch out, the -documentation will tell you so. Some of the security concerns of web -development are far more complex than one might think and often we all end -up in situations where we think "well, this is just far fetched, how could -that possibly be exploited" and then an intelligent guy comes along and -figures a way out to exploit that application. And don't think, your -application is not important enough for hackers to take notice. Depending -on the kind of attack, chances are there are automated botnets out there -trying to figure out how to fill your database with viagra advertisements. +The documentation will warn you about aspects of web development that +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited, until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. -So always keep that in mind when doing web development. +So always keep security in mind when doing web development. Target Audience --------------- -Is Flask for you? If your application small-ish and does not depend on -too complex database structures, Flask is the Framework for you. It was -designed from the ground up to be easy to use, based on established -principles, good intentions and on top of two established libraries in -widespread usage. Recent versions of Flask scale nicely within reasonable -bounds and if you grow larger, you won't have any troubles adjusting Flask +Is Flask for you? If your application is small-ish and does not depend on +very complex database structures, Flask is the Framework for you. It was +designed from the ground up to be easy to use, and built on the firm +foundation of established principles, good intentions, and mature, widely +used libraries. Recent versions of Flask scale nicely within reasonable +bounds, and if you grow larger, you won't have any trouble adjusting Flask for your new application size. If you suddenly discover that your application grows larger than originally intended, head over to the :ref:`becomingbig` section to see some possible solutions for larger applications. -Satisfied? Then head over to the :ref:`installation`. +Satisfied? Then let's proceed with :ref:`installation`. From f5e533076fb6f0102bcba9ddb24f905045fa6504 Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 14:39:16 +0800 Subject: [PATCH 0292/3747] Edited the installation guide. --- docs/installation.rst | 137 ++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 78fb103f..040412c6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,44 +3,41 @@ Installation ============ -Flask is a microframework and yet it depends on external libraries. There -are various ways how you can install that library and this explains each -way and why there are multiple ways. - -Flask depends on two external libraries: `Werkzeug +Flask depends on two external libraries, `Werkzeug `_ and `Jinja2 `_. -The first one is responsible for interfacing WSGI the latter for rendering -templates. Now you are maybe asking, what is WSGI? WSGI is a standard -in Python that is basically responsible for ensuring that your application -is behaving in a specific way so that you can run it on different -environments (for example on a local development server, on an Apache2, on -lighttpd, on Google's App Engine or whatever you have in mind). +Werkzeug is a toolkit for WSGI, the standard Python interface between web +applications and a variety of servers for both development and deployment. +Jinja2 renders templates. -So how do you get all that on your computer in no time? The most kick-ass -method is virtualenv, so let's look at that first. +So how do you get all that on your computer quickly? There are many ways +which this section will explain, but the most kick-ass method is +virtualenv, so let's look at that first. .. _virtualenv: virtualenv ---------- -Virtualenv is what you want to use during development and in production if -you have shell access. So first: what does virtualenv do? If you are -like me and you like Python, chances are you want to use it for another -project as well. Now the more projects you have, the more likely it is -that you will be working with different versions of Python itself or at -least an individual library. Because let's face it: quite often libraries -break backwards compatibility and it's unlikely that your application will -not have any dependencies, that just won't happen. So virtualenv to the -rescue! +Virtualenv is probably what you want to use during development, and in +production too if you have shell access there. -It basically makes it possible to have multiple side-by-side -"installations" of Python, each for your own project. It's not actually -an installation but a clever way to keep things separated. +What problem does virtualenv solve? If you like Python as I do, +chances are you want to use it for other projects besides Flask-based +web applications. But the more projects you have, the more likely it is +that you will be working with different versions of Python itself, or at +least different versions of Python libraries. Let's face it; quite often +libraries break backwards compatibility, and it's unlikely that any serious +application will have zero dependencies. So what do you do if two or more +of your projects have conflicting dependencies? -So let's see how that works! +Virtualenv to the rescue! It basically enables multiple side-by-side +installations of Python, one for each project. It doesn't actually +install separate copies of Python, but it does provide a clever way +to keep different project environments isolated. -If you are on OS X or Linux chances are that one of the following two +So let's see how virtualenv works! + +If you are on Mac OS X or Linux, chances are that one of the following two commands will work for you:: $ sudo easy_install virtualenv @@ -49,18 +46,19 @@ or even better:: $ sudo pip install virtualenv -Chances are you have virtualenv installed on your system then. Maybe it's -even in your package manager (on ubuntu try ``sudo apt-get install -python-virtualenv``). +One of these will probably install virtualenv on your system. Maybe it's +even in your package manager. If you use Ubuntu, try:: -If you are on Windows and missing the `easy_install` command you have to + $ sudo apt-get install python-virtualenv + +If you are on Windows and don't have the `easy_install` command, you must install it first. Check the :ref:`windows-easy-install` section for more information about how to do that. Once you have it installed, run the -same commands as above, but without the `sudo` part. +same commands as above, but without the `sudo` prefix. -So now that you have virtualenv running just fire up a shell and create -your own environment. I usually create a folder and a `env` folder -within:: +Once you have virtualenv installed, just fire up a shell and create +your own environment. I usually create a project folder and an `env` +folder within:: $ mkdir myproject $ cd myproject @@ -68,14 +66,14 @@ within:: New python executable in env/bin/python Installing setuptools............done. -Now you only have to activate it, whenever you work with it. On OS X and -Linux do the following:: +Now, whenever you want to work on a project, you only have to activate +the corresponding environment. On OS X and Linux, do the following:: $ . env/bin/activate -(Note the whitespace between the dot and the script name. This means -execute this file in context of the shell. If the dot does not work for -whatever reason in your shell, try substituting it with ``source``) +(Note the space between the dot and the script name. The dot means that +this script should run in the context of the current shell. If this command +does not work in your shell, try replacing the dot with ``source``) If you are a Windows user, the following command is for you:: @@ -95,23 +93,22 @@ A few seconds later you are good to go. System Wide Installation ------------------------ -This is possible as well, but I would not recommend it. Just run +This is possible as well, but I do not recommend it. Just run `easy_install` with root rights:: - sudo easy_install Flask + $ sudo easy_install Flask -(Run it in an Admin shell on Windows systems and without the `sudo`). +(Run it in an Admin shell on Windows systems and without `sudo`). Living on the Edge ------------------ -You want to work with the latest version of Flask, there are two ways: you -can either let `easy_install` pull in the development version or tell it -to operate on a git checkout. Either way it's recommended to do that in a -virtualenv. +If you want to work with the latest version of Flask, there are two ways: you +can either let `easy_install` pull in the development version, or tell it +to operate on a git checkout. Either way, virtualenv is recommended. -Get the git checkout in a new virtualenv and run in develop mode:: +Get the git checkout in a new virtualenv and run in development mode:: $ git clone http://github.com/mitsuhiko/flask.git Initialized empty Git repository in ~/dev/flask/.git/ @@ -124,9 +121,9 @@ Get the git checkout in a new virtualenv and run in develop mode:: ... Finished processing dependencies for Flask -This will pull in the dependencies and activate the git head as current -version. Then you just have to ``git pull origin`` to get the latest -version. +This will pull in the dependencies and activate the git head as the current +version inside the virtualenv. Then you just have to ``git pull origin`` +to get the latest version. To just get the development version without git, do this instead:: @@ -145,31 +142,29 @@ To just get the development version without git, do this instead:: `easy_install` on Windows ------------------------- -On Windows installation of `easy_install` is a little bit tricker because -on Windows slightly different rules apply, but it's not a biggy. The -easiest way to accomplish that is downloading the `ez_setup.py`_ file and -running it. (Double clicking should do the trick) +On Windows, installation of `easy_install` is a little bit tricker because +slightly different rules apply on Windows than on Unix-like systems, but +it's not difficult. The easiest way to do it is to download the +`ez_setup.py`_ file and run it. The easiest way to run the file is to +open your downloads folder and double-click on the file. -Once you have done that it's important to add the `easy_install` command -and other Python scripts to the path. To do that you have to add the -Python installation's Script folder to the `PATH` variable. - -To do that, right-click on your "Computer" desktop icon and click -"Properties". On Windows Vista and Windows 7 then click on "Advanced System -settings", on Windows XP click on the "Advanced" tab instead. Then click +Next, add the `easy_install` command and other Python scripts to the +command search path, by adding your Python installation's Scripts folder +to the `PATH` environment variable. To do that, right-click on the +"Computer" icon on the Desktop or in the Start menu, and choose +"Properties". Then, on Windows Vista and Windows 7 click on "Advanced System +settings"; on Windows XP, click on the "Advanced" tab instead. Then click on the "Environment variables" button and double click on the "Path" -variable in the "System variables" section. - -There append the path of your Python interpreter's Script folder to the -end of the last (make sure you delimit it from existing values with a -semicolon). Assuming you are using Python 2.6 on the default path, add -the following value:: +variable in the "System variables" section. There append the path of your +Python interpreter's Scripts folder; make sure you delimit it from +existing values with a semicolon. Assuming you are using Python 2.6 on +the default path, add the following value:: ;C:\Python26\Scripts -Then you are done. To check that it worked, open the cmd and execute -"easy_install". If you have UAC enabled it should prompt you for admin -privileges. +Then you are done. To check that it worked, open the Command Prompt and +execute ``easy_install``. If you have User Account Control enabled on +Windows Vista or Windows 7, it should prompt you for admin privileges. .. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py From 4a0c0446da3fc278a5cdc2587191f6df66489d02 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 26 Jun 2010 03:07:19 -0700 Subject: [PATCH 0293/3747] Added Matt Campell --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c3c0e13b..a3ddd0f2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Patches and Suggestions - Justin Quick - Kenneth Reitz - Marian Sigler +- Matt Campell - Matthew Frazier - Ron DuPlain - Sebastien Estienne From 67f3f1bac5cfadfa5398e18dd232d279576cf85c Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Sat, 26 Jun 2010 22:43:14 +0800 Subject: [PATCH 0294/3747] Track the docs/_themes directory --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..b8e0f4f1 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit b8e0f4f1bfc7c89fffb5440fcdf60edaa033c836 From 824c10c6e4d9c2235d41a2276a0158b1456c3b06 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 17:37:04 +0800 Subject: [PATCH 0295/3747] Added MongoKit pattern and fixed typo in sqlalchemy pattern --- docs/patterns/mongokit.rst | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/patterns/mongokit.rst diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst new file mode 100644 index 00000000..5f6b3b94 --- /dev/null +++ b/docs/patterns/mongokit.rst @@ -0,0 +1,133 @@ +.. mongokit-pattern: + +MongoKit in Flask +================= + +Using a document database rather than a full DBMS gets more common these days. +This pattern shows how to use MongoKit, a document mapper library, to +integrate with MongoDB. + +This pattern requires an running MongoDB server and the MongoKit library +installed. + +There are two very common ways to use MongoKit. I will outline each of them +here: + + +Declarative +----------- + +The default behaviour of MongoKit is the declarative one that is based on +common ideas from Django or the SQLAlchemy declarative extension. + +Here an example `database.py` module for your application:: + + from mongokit import Connection, Document + + connection = Connection() + + +To define your models, just subclass the `Document` class that is imported +from MongoKit. If you've seen the SQLAlchemy pattern you may wonder why we do +not have a session and even do not define a `init_db` function here. On the +one hand, MongoKit does not have something like a session. This sometimes +makes it more to type but also makes it blazingly fast. On the other hand, +MongoDB is schemaless. This means you can modify the data structure from one +insert query to the next without any problem. MongoKit is just schemaless +too, but implements some validation to ensure data integrity. + +Here is an example document (put this into `models.py`, e.g.):: + + from yourapplication.database import Document, connection + + def max_length(length): + def validate(value): + if len(value) <= length: + return True + raise Exception('%s must be at most %s characters long' % length) + return validate + + class User(Document): + structure = { + 'name': unicode, + 'email': unicode, + } + validators = { + 'name': max_length(50), + 'email': max_length(120) + } + use_dot_notation = True + def __repr__(self): + return '' % (self.name) + + # register the User document with our current connection + connection.register([User]) + + +This example shows you how to define your schema (named structure), a +validator for the maximum character length and uses a special MongoKit feature +called `use_dot_notation`. Per default MongoKit behaves like a python +dictionary but with `use_dot_notation` set to `True` you can use your +documents like you use models in nearly any other ORM by using dots to +seperate between attributes. + +You can insert entries into the database like this: + +>>> from yourapplication.database import connection +>>> from yourapplication.models import User +>>> collection = connection['test'].users +>>> user = collection.User() +>>> user['name'] = u'admin' +>>> user['email'] = u'admin@localhost' +>>> user.save() + +Note that MongoKit is kinda strict with used column types, you must not use a +common `str` type for either `name` or `email` but unicode. + +Querying is simple as well: + +>>> list(collection.User.find()) +[] +>>> collection.User.find_one({'name': u'admin'}) + + +.. _MongoKit: http://bytebucket.org/namlook/mongokit/ + + +PyMongo Compatibility Layer +--------------------------- + +If you just want to use PyMongo, you can do that with MongoKit as well. You +may use this process if you need the best performance to get:: + + from MongoKit import Connection + + connection = Connection() + +To insert data you can use the `insert` method. We have to get a +collection first, this is somewhat the same as a table in the SQL world. + +>>> collection = connection['test'].users +>>> user = {'name': u'admin', 'email': u'admin@localhost'} +>>> collection.insert(user) + +print list(collection.find()) +print collection.find_one({'name': u'admin'}) + +MongoKit will automatically commit for us. + +To query your database, you use the collection directly: + +>>> list(collection.find()) +[{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'}] +>>> collection.find_one({'name': u'admin'}) +{u'_id': ObjectId('4c271729e13823182f000000'), u'name': u'admin', u'email': u'admin@localhost'} + +These results are also dict-like objects: + +>>> r = collection.find_one({'name': u'admin'}) +>>> r['email'] +u'admin@localhost' + +For more information about MongoKit, head over to the +`website `_. From b5db6bf529706da651389ef51b0fce87a89a47e8 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 17:38:39 +0800 Subject: [PATCH 0296/3747] now really pushed the fix in sqlalchemy pattern --- docs/patterns/index.rst | 1 + docs/patterns/sqlalchemy.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 2a8c0af7..23b20dc5 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -30,3 +30,4 @@ Snippet Archives `_. jquery errorpages lazyloading + mongokit diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 3945c1fa..6a14c8e0 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -68,7 +68,7 @@ Here is an example model (put this into `models.py`, e.g.):: self.email = email def __repr__(self): - return '' % (self.name, self.email) + return '' % (self.name) You can insert entries into the database like this: From 87c2c794421f8a39aa98a35f518613cfd38852d5 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 27 Jun 2010 22:03:43 +0800 Subject: [PATCH 0297/3747] fixed mongokit pattern to show how the flask way to configure hostname and port --- docs/patterns/mongokit.rst | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index 5f6b3b94..27f182a3 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -20,11 +20,22 @@ Declarative The default behaviour of MongoKit is the declarative one that is based on common ideas from Django or the SQLAlchemy declarative extension. -Here an example `database.py` module for your application:: +Here an example `app.py` module for your application:: + from flask import Flask from mongokit import Connection, Document - connection = Connection() + # configuration + MONGODB_HOST = 'localhost' + MONGODB_PORT = 27017 + + # create the little application object + app = Flask(__name__) + app.config.from_object(__name__) + + # connect to the database + connection = Connection(app.config['MONGODB_HOST'], + app.config['MONGODB_PORT']) To define your models, just subclass the `Document` class that is imported @@ -36,10 +47,8 @@ MongoDB is schemaless. This means you can modify the data structure from one insert query to the next without any problem. MongoKit is just schemaless too, but implements some validation to ensure data integrity. -Here is an example document (put this into `models.py`, e.g.):: +Here is an example document (put this also into `app.py`, e.g.):: - from yourapplication.database import Document, connection - def max_length(length): def validate(value): if len(value) <= length: @@ -98,7 +107,9 @@ PyMongo Compatibility Layer --------------------------- If you just want to use PyMongo, you can do that with MongoKit as well. You -may use this process if you need the best performance to get:: +may use this process if you need the best performance to get. Note that this +example does not show how to couple it with Flask, see the above MongoKit code +for examples:: from MongoKit import Connection From 5ede53066fc26d993dd229fe0c54209e700f4c09 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:10:07 +0800 Subject: [PATCH 0298/3747] Minor tutorial documentation fixes (grammar, etc) --- docs/tutorial/setup.rst | 4 ++-- docs/tutorial/views.rst | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 790cc41e..b5ae2a0e 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -52,7 +52,7 @@ The `secret_key` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The debug flag enables or disables the interactive debugger. Never leave debug mode activated in a production system because it will allow users to -executed code on the server! +execute code on the server! We also add a method to easily connect to the database specified. That can be used to open a connection on request and also from the interactive @@ -64,7 +64,7 @@ Python shell or a script. This will come in handy later return sqlite3.connect(app.config['DATABASE']) Finally we just add a line to the bottom of the file that fires up the -server if we run that file as standalone application:: +server if we want to run that file as a standalone application:: if __name__ == '__main__': app.run() diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index c7cc1ed4..0bce03a3 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -11,11 +11,11 @@ Show Entries This view shows all the entries stored in the database. It listens on the root of the application and will select title and text from the database. -The one with the highest id (the newest entry) on top. The rows returned -from the cursor are tuples with the columns ordered like specified in the -select statement. This is good enough for small applications like here, -but you might want to convert them into a dict. If you are interested how -to do that, check out the :ref:`easy-querying` example. +The one with the highest id (the newest entry) will be on top. The rows +returned from the cursor are tuples with the columns ordered like specified +in the select statement. This is good enough for small applications like +here, but you might want to convert them into a dict. If you are +interested in how to do that, check out the :ref:`easy-querying` example. The view function will pass the entries as dicts to the `show_entries.html` template and return the rendered one:: @@ -53,11 +53,11 @@ Login and Logout These functions are used to sign the user in and out. Login checks the username and password against the ones from the configuration and sets the -`logged_in` key in the session. If the user logged in successfully that -key is set to `True` and the user is redirected back to the `show_entries` -page. In that case also a message is flashed that informs the user he or -she was logged in successfully. If an error occoured the template is -notified about that and the user asked again:: +`logged_in` key in the session. If the user logged in successfully, that +key is set to `True`, and the user is redirected back to the `show_entries` +page. In addition, a message is flashed that informs the user that he or +she was logged in successfully. If an error occurred, the template is +notified about that, and the user is asked again:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -73,12 +73,12 @@ notified about that and the user asked again:: return redirect(url_for('show_entries')) return render_template('login.html', error=error) -The logout function on the other hand removes that key from the session +The logout function, on the other hand, removes that key from the session again. We use a neat trick here: if you use the :meth:`~dict.pop` method -of the dict and pass a second parameter to it (the default) the method +of the dict and pass a second parameter to it (the default), the method will delete the key from the dictionary if present or do nothing when that -key was not in there. This is helpful because we don't have to check in -that case if the user was logged in. +key is not in there. This is helpful because now we don't have to check +if the user was logged in. :: From 505c530c9a4c8f1b6b063bcb481a4fdcf3897709 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:24:37 +0800 Subject: [PATCH 0299/3747] Minor testing documentation fixes (grammar, etc) --- docs/testing.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index f9785b18..d131c673 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -8,8 +8,8 @@ Testing Flask Applications Not sure where that is coming from, and it's not entirely correct, but also not that far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to -become pretty paranoid. If an application however has automated tests, you -can safely change things and you will instantly know if your change broke +become pretty paranoid. If an application has automated tests, you can +safely change things, and you will instantly know if your change broke something. Flask gives you a couple of ways to test applications. It mainly does @@ -60,7 +60,7 @@ each individual test function. To delete the database after the test, we close the file and remove it from the filesystem in the :meth:`~unittest.TestCase.tearDown` method. What the test client does is give us a simple interface to the application. We can trigger test -requests to the application and the client will also keep track of cookies +requests to the application, and the client will also keep track of cookies for us. Because SQLite3 is filesystem-based we can easily use the tempfile module @@ -130,7 +130,7 @@ Logging In and Out ------------------ The majority of the functionality of our application is only available for -the administration user. So we need a way to log our test client in to the +the administrative user, so we need a way to log our test client in to the application and out of it again. For that we fire some requests to the login and logout pages with the required form data (username and password). Because the login and logout pages redirect, we tell the @@ -200,12 +200,12 @@ suite. Other Testing Tricks -------------------- -Besides using the test client we used above there is also the +Besides using the test client we used above, there is also the :meth:`~flask.Flask.test_request_context` method that in combination with the `with` statement can be used to activate a request context temporarily. With that you can access the :class:`~flask.request`, :class:`~flask.g` and :class:`~flask.session` objects like in view -functions. Here a full example that showcases this:: +functions. Here's a full example that showcases this:: app = flask.Flask(__name__) From ee5eafa7951bf636bdf345cb072f89947e265867 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:43:44 +0800 Subject: [PATCH 0300/3747] Error Handling documentation fixes (grammar, etc) --- docs/errorhandling.rst | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index cc7437a6..ac9595f1 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -5,7 +5,7 @@ Handling Application Errors .. versionadded:: 0.3 -Applications fail, server fail. Sooner or later you will see an exception +Applications fail, servers fail. Sooner or later you will see an exception in production. Even if your code is 100% correct, you will still see exceptions from time to time. Why? Because everything else involved will fail. Here some situations where perfectly fine code can lead to server @@ -20,7 +20,7 @@ errors: - a programming error in a library you are using - network connection of the server to another system failed. -And that's just a small sample of issues you could be facing. So how to +And that's just a small sample of issues you could be facing. So how do we deal with that sort of problem? By default if your application runs in production mode, Flask will display a very simple page for you and log the exception to the :attr:`~flask.Flask.logger`. @@ -32,10 +32,10 @@ Error Mails ----------- If the application runs in production mode (which it will do on your -server) you won't see any log messages by default. Why that? Flask tries -to be a zero-configuration framework and where should it drop the logs for -you if there is no configuration. Guessing is not a good idea because -chances are, the place it guessed is not the place where the user has the +server) you won't see any log messages by default. Why is that? Flask +tries to be a zero-configuration framework. Where should it drop the logs +for you if there is no configuration? Guessing is not a good idea because +chances are, the place it guessed is not the place where the user has permission to create a logfile. Also, for most small applications nobody will look at the logs anyways. @@ -45,9 +45,9 @@ when a user reported it for you. What you want instead is a mail the second the exception happened. Then you get an alert and you can do something about it. -Flask is using the Python builtin logging system and that one can actually -send you mails for errors which is probably what you want. Here is how -you can configure the Flask logger to send you mails for exceptions:: +Flask uses the Python builtin logging system, and it can actually send +you mails for errors which is probably what you want. Here is how you can +configure the Flask logger to send you mails for exceptions:: ADMINS = ['yourname@example.com'] if not app.debug: @@ -63,8 +63,9 @@ So what just happened? We created a new :class:`~logging.handlers.SMTPHandler` that will send mails with the mail server listening on ``127.0.0.1`` to all the `ADMINS` from the address *server-error@example.com* with the subject "YourApplication Failed". If -your mail server requires credentials these can also provided, for that -check out the documentation for the :class:`~logging.handlers.SMTPHandler`. +your mail server requires credentials, these can also be provided. For +that check out the documentation for the +:class:`~logging.handlers.SMTPHandler`. We also tell the handler to only send errors and more critical messages. Because we certainly don't want to get a mail for warnings or other @@ -115,12 +116,12 @@ Controlling the Log Format -------------------------- By default a handler will only write the message string into a file or -send you that message as mail. But a log record stores more information +send you that message as mail. A log record stores more information, and it makes a lot of sense to configure your logger to also contain that information so that you have a better idea of why that error happened, and more importantly, where it did. -A formatter can be instanciated with a format string. Note that +A formatter can be instantiated with a format string. Note that tracebacks are appended to the log entry automatically. You don't have to do that in the log formatter format string. @@ -206,7 +207,7 @@ formatter. The formatter has three interesting methods: called for `asctime` formatting. If you want a different time format you can override this method. :meth:`~logging.Formatter.formatException` - called for exception formatting. It is passed a :attr:`~sys.exc_info` + called for exception formatting. It is passed an :attr:`~sys.exc_info` tuple and has to return a string. The default is usually fine, you don't have to override it. @@ -217,8 +218,8 @@ Other Libraries --------------- So far we only configured the logger your application created itself. -Other libraries might log themselves as well. For example, SQLAlchemy use -logging heavily in the core. While there is a method to configure all +Other libraries might log themselves as well. For example, SQLAlchemy uses +logging heavily in its core. While there is a method to configure all loggers at once in the :mod:`logging` package, I would not recommend using it. There might be a situation in which you want to have multiple separate applications running side by side in the same Python interpreter From 24754114fc023c92385a1f9bfef141d93a9c8f9b Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:49:50 +0800 Subject: [PATCH 0301/3747] Minor config documentation fixes (grammar, etc) --- docs/config.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index e057ba4d..73d7c8fb 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -6,12 +6,12 @@ Configuration Handling .. versionadded:: 0.3 Applications need some kind of configuration. There are different things -you might want to change. Like toggling debug mode, the secret key and a +you might want to change like toggling debug mode, the secret key, and a lot of very similar things. The way Flask is designed usually requires the configuration to be -available when the application starts up. You can either hardcode the -configuration in the code which for many small applications is not +available when the application starts up. You can hardcode the +configuration in the code, which for many small applications is not actually that bad, but there are better ways. Independent of how you load your config, there is a config object @@ -64,8 +64,8 @@ The following configuration values are used internally by Flask: Configuring from Files ---------------------- -Configuration becomes more useful if you can configure from a file. And -ideally that file would be outside of the actual application package that +Configuration becomes more useful if you can configure from a file, and +ideally that file would be outside of the actual application package so that you can install the package with distribute (:ref:`distribute-deployment`) and still modify that file afterwards. @@ -75,7 +75,7 @@ So a common pattern is this:: app.config.from_object('yourapplication.default_settings') app.config.from_envvar('YOURAPPLICATION_SETTINGS') -What this does is first loading the configuration from the +This first loads the configuration from the `yourapplication.default_settings` module and then overrides the values with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` environment variable points to. This environment variable can be set on @@ -95,7 +95,7 @@ The configuration files themselves are actual Python files. Only values in uppercase are actually stored in the config object later on. So make sure to use uppercase letters for your config keys. -Here an example configuration file:: +Here is an example configuration file:: DEBUG = False SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' From 4ed7a923f53c71339251dd1ccd681f424b061bd1 Mon Sep 17 00:00:00 2001 From: Adam Zapletal Date: Mon, 28 Jun 2010 11:52:32 +0800 Subject: [PATCH 0302/3747] Minor sqlite3 documentation fixes (grammar, etc) --- docs/patterns/sqlite3.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index c11e837d..1032097e 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,12 +3,12 @@ Using SQLite 3 with Flask ========================= -In Flask you can implement opening of database connections at the beginning -of the request and closing at the end with the +In Flask you can implement the opening of database connections at the +beginning of the request and closing at the end with the :meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` decorators in combination with the special :class:`~flask.g` object. -So here a simple example of how you can use SQLite 3 with Flask:: +So here is a simple example of how you can use SQLite 3 with Flask:: import sqlite3 from flask import g @@ -33,7 +33,7 @@ Easy Querying ------------- Now in each request handling function you can access `g.db` to get the -current open database connection. To simplify working with SQLite a +current open database connection. To simplify working with SQLite, a helper function can be useful:: def query_db(query, args=(), one=False): From 55040d3efa2d608eb4daa1ed7086e0bd36cfd252 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Jun 2010 00:13:48 -0700 Subject: [PATCH 0303/3747] Added Adam Zapletal and Christopher Grebs to AUTHORs file. --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index a3ddd0f2..e20a3735 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,8 +9,10 @@ Development Lead Patches and Suggestions ``````````````````````` +- Adam Zapletal - Chris Edgemon - Chris Grindstaff +- Christopher Grebs - Florent Xicluna - Georg Brandl - Justin Quick From f195d9244756b9809bd2b00601b437f3e278b0b9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:13:40 +0200 Subject: [PATCH 0304/3747] Added proper subdomain support --- CHANGES | 4 ++++ docs/config.rst | 9 +++++++++ flask.py | 6 ++++-- tests/flask_tests.py | 21 +++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index d77a1f50..3490f0e2 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.5 Codename to be decided, release date to be announced. +- fixed a bug with subdomains that was caused by the inability to + specify the server name. The server name can now be set with + the `SERVER_NAME` config key. + Version 0.4 ----------- diff --git a/docs/config.rst b/docs/config.rst index 73d7c8fb..fabf6dc4 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -59,8 +59,17 @@ The following configuration values are used internally by Flask: ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile +``LOGGER_NAME`` the name of the logger +``SERVER_NAME`` the name of the server. Required for + subdomain support (eg: ``'localhost'``) =============================== ========================================= +.. versionadded:: 0.4 + ``LOGGER_NAME`` + +.. versionadded:: 0.5 + ``SERVER_NAME`` + Configuring from Files ---------------------- diff --git a/flask.py b/flask.py index a769692e..34e903cc 100644 --- a/flask.py +++ b/flask.py @@ -141,7 +141,8 @@ class _RequestContext(object): def __init__(self, app, environ): self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ) + self.url_adapter = app.url_map.bind_to_environ(environ, + server_name=app.config['SERVER_NAME']) self.request = app.request_class(environ) self.session = app.open_session(self.request) if self.session is None: @@ -889,7 +890,8 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, - 'LOGGER_NAME': None + 'LOGGER_NAME': None, + 'SERVER_NAME': None }) def __init__(self, import_name): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f0f15a7b..cbf2711f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -836,6 +836,26 @@ class ConfigTestCase(unittest.TestCase): os.environ = env +class SubdomainTestCase(unittest.TestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + assert rv.data == 'normal index' + + rv = c.get('/', 'http://test.localhost/') + assert rv.data == 'test index' + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -847,6 +867,7 @@ def suite(): suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) From 7ea1b801cc1f7d09ee18e8d70957c9d34b870700 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:32:02 +0200 Subject: [PATCH 0305/3747] Documented missing attributes. This fixes #71 --- flask.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 34e903cc..d2a0298c 100644 --- a/flask.py +++ b/flask.py @@ -67,7 +67,21 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - endpoint = view_args = routing_exception = None + #: the endpoint that matched the request. This in combination with + #: :attr:`view_args` can be used to reconstruct the same or a + #: modified URL. If an exception happened when matching, this will + #: be `None`. + endpoint = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None @property def module(self): From bcd746e8cf0fc5f405c5f32b80517a4390de6fd9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 01:36:06 +0200 Subject: [PATCH 0306/3747] Added another testcase for subdomain support --- tests/flask_tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index cbf2711f..1c5dc729 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -855,6 +855,17 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + assert rv.data == 'index for mitsuhiko' + def suite(): from minitwit_tests import MiniTwitTestCase From 9373a71c263a63eca3c466cbc5b175ed5d4999d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 15:25:20 +0200 Subject: [PATCH 0307/3747] improved _request_ctx_stack docs --- docs/api.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index fc6f68fb..4c45c2dd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -301,6 +301,36 @@ Useful Internals instance and can be used by extensions and application code but the use is discouraged in general. + The following attributes are always present on each layer of the + stack:: + + `app` + the active Flask application. + + `url_adapter` + the URL adapter that was used to match the request. + + `request` + the current request object. + + `session` + the active session object. + + `g` + an object with all the attributes of the :data:`flask.g` object. + + `flashes` + an internal cache for the flashed messages. + + Example usage:: + + from flask import _request_ctx_stack + + def get_session(): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.session + .. versionchanged:: 0.4 The request context is automatically popped at the end of the request From 881aa3ab2dad11edcddd0d5996fea9bdc3132224 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 29 Jun 2010 15:59:55 +0200 Subject: [PATCH 0308/3747] Fixed an rst syntax error --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 4c45c2dd..b6c81d10 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -302,7 +302,7 @@ Useful Internals use is discouraged in general. The following attributes are always present on each layer of the - stack:: + stack: `app` the active Flask application. From f5d2d324b5a1cc9d7ab14ab40488d8b7f4b8c616 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 30 Jun 2010 09:49:04 +0200 Subject: [PATCH 0309/3747] Mention virtualenv in mod_wsgi deployment docs --- docs/deploying/mod_wsgi.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index b4753c3e..b2d25f5c 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -133,3 +133,19 @@ If your application does not run, follow this guide to troubleshoot: The reason for this is that for non-installed packages, the module filename is used to locate the resources and for symlinks the wrong filename is picked up. + +Working with Virtual Environments +--------------------------------- + +Virtual environments have the advantage that they never install the +required dependencies system wide so you have a better control over what +is used where. If you want to use a virtual environment with mod_wsgi you +have to modify your `.wsgi` file slightly. + +Add the following lines to the top of your `.wsgi` file:: + + activate_this = '/path/to/env/bin/activate_this.py' + execfile(activate_this, dict(__file__=activate_this)) + +This sets up the load paths according to the settings of the virtual +environment. Keep in mind that the path has to be absolute. From e75322206d2fd1c483494fea7bfba367cdcdd9b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 00:19:32 +0200 Subject: [PATCH 0310/3747] Fixed a broken example in the docs --- docs/quickstart.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 11b0cd84..fd9b02c2 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -628,7 +628,9 @@ unless he knows the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: - from flask import session, redirect, url_for, escape + from flask import Flask, session, redirect, url_for, escape, request + + app = Flask(__name__) @app.route('/') def index(): @@ -652,6 +654,7 @@ sessions work:: def logout(): # remove the username from the session if its there session.pop('username', None) + return redirect(url_for('index')) # set the secret key. keep this really secret: app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' From d0357b44b02324b85af42052487eab13d0b75f81 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 01:22:46 +0200 Subject: [PATCH 0311/3747] Added links to Flask-WTF and Flask-SQLAlchemy. This fixes #73 --- docs/patterns/sqlalchemy.rst | 16 +++++++++++++++- docs/patterns/wtforms.rst | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 6a14c8e0..a66627ad 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -8,9 +8,23 @@ encouraged to use a package instead of a module for your flask application and drop the models into a separate module (:ref:`larger-applications`). While that is not necessary, it makes a lot of sense. -There are three very common ways to use SQLAlchemy. I will outline each +There are four very common ways to use SQLAlchemy. I will outline each of them here: +Flask-SQLAlchemy Extension +-------------------------- + +Because SQLAlchemy is a common database abstraction layer and object +relational mapper that requires a little bit of configuration effort, +there is a Flask extension that handles that for you. This is recommended +if you want to get started quickly. + +You can download `Flask-SQLAlchemy`_ from `PyPI +`_. + +.. _Flask-SQLAlchemy: http://packages.python.org/Flask-SQLAlchemy/ + + Declarative ----------- diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index d62c5bd3..8a113cc5 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -12,6 +12,15 @@ first. I recommend breaking up the application into multiple modules (:ref:`larger-applications`) for that and adding a separate module for the forms. +.. admonition:: Getting most of WTForms with an Extension + + The `Flask-WTF`_ extension expands on this pattern and adds a few + handful little helpers that make working with forms and Flask more + fun. You can get it from `PyPI + `_. + +.. _Flask-WTF: http://packages.python.org/Flask-WTF/ + The Forms --------- From bc662a546ed9028d93482f79314e64beae19a1d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 01:45:39 +0200 Subject: [PATCH 0312/3747] Added a section about unicode and editors. This fixes #74 --- docs/_themes | 2 +- docs/unicode.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/_themes b/docs/_themes index b8e0f4f1..21cf0743 160000 --- a/docs/_themes +++ b/docs/_themes @@ -1 +1 @@ -Subproject commit b8e0f4f1bfc7c89fffb5440fcdf60edaa033c836 +Subproject commit 21cf07433147212ee6c8ab203dfa648a9239c66f diff --git a/docs/unicode.rst b/docs/unicode.rst index e9259d12..7db462b8 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -54,6 +54,8 @@ unicode. What does working with unicode in Python 2.x mean? UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of your Python source file. +- Jinja is configured to decode the template files from UTF08. So make + sure to tell your editor to save the file as UTF-8 there as well. Encoding and Decoding Yourself ------------------------------ @@ -79,3 +81,27 @@ To go from unicode into a specific charset such as UTF-8 you can use the def write_file(filename, contents, charset='utf-8'): with open(filename, 'w') as f: f.write(contents.encode(charset)) + +Configuring Editors +------------------- + +Most editors save as UTF-8 by default nowadays but in case your editor is +not configured to do this you have to change it. Here some common ways to +set your editor to store as UTF-8: + +- Vim: put ``set enc=utf-8`` to your ``.vimrc`` file. + +- Emacs: either use an encoding cookie or put this into your ``.emacs`` + file:: + + (prefer-coding-system 'utf-8) + (setq default-buffer-file-coding-system 'utf-8) + +- Notepad++: + + 1. Go to *Settings -> Preferences ...* + 2. Select the "New Document/Default Directory" tab + 3. Select "UTF-8 without BOM" as encoding + + It is also recommended to use the Unix newline format, you can select + it in the same panel but this not a requirement. From 3ab318a7ddca819e13d6d161b008b62b6583453e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Jul 2010 12:56:34 +0200 Subject: [PATCH 0313/3747] Explained Flask constructor better. This fixes #70 --- docs/quickstart.rst | 7 ++++++- flask.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fd9b02c2..a4858f1c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -38,7 +38,12 @@ see your hello world greeting. So what did that code do? 1. first we imported the :class:`~flask.Flask` class. An instance of this - class will be our WSGI application. + class will be our WSGI application. The first argument is the name of + the application's module. If you are using a single module (like here) + you should use `__name__` because depending on if it's started as + application or imported as module the name will be different + (``'__main__'`` versus the actual import name). For more information + on that, have a look at the :class:`~flask.Flask` documentation. 2. next we create an instance of it. We pass it the name of the module / package. This is needed so that Flask knows where it should look for templates, static files and so on. diff --git a/flask.py b/flask.py index d2a0298c..90867fcc 100644 --- a/flask.py +++ b/flask.py @@ -806,6 +806,34 @@ class Flask(_PackageBoundObject): from flask import Flask app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplicaiton.app` and not + `yourapplication.views.frontend`) """ #: The class that is used for request objects. See :class:`~flask.Request` From a154c87cfca3e21316552b2ec558029f9c96122a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 2 Jul 2010 19:45:26 +0200 Subject: [PATCH 0314/3747] Documented exception catching behaviour. This fixes #75 --- flask.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flask.py b/flask.py index 90867fcc..9c720ef0 100644 --- a/flask.py +++ b/flask.py @@ -1090,6 +1090,16 @@ class Flask(_PackageBoundObject): :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. + .. admonition:: Keep in Mind + + Flask will supress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you ahve to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` to have the server available externally as well. :param port: the port of the webserver From ee16a68bbd7caf4c8e79c5d6859f72ad38539bbf Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 14:19:26 -0400 Subject: [PATCH 0315/3747] out with the old --- flask.py | 1573 ------------------------------------------------------ 1 file changed, 1573 deletions(-) delete mode 100644 flask.py diff --git a/flask.py b/flask.py deleted file mode 100644 index 9c720ef0..00000000 --- a/flask.py +++ /dev/null @@ -1,1573 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask - ~~~~~ - - A microframework based on Werkzeug. It's extensively documented - and follows best practice patterns. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import with_statement -import os -import sys -import mimetypes -from datetime import datetime, timedelta - -# this is a workaround for appengine. Do not remove this import -import werkzeug - -from itertools import chain -from threading import Lock -from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers, \ - import_string -from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError -from werkzeug.contrib.securecookie import SecureCookie - -# try to load the best simplejson implementation available. If JSON -# is not installed, we add a failing class. -json_available = True -try: - import simplejson as json -except ImportError: - try: - import json - except ImportError: - json_available = False - -# utilities we import from Werkzeug and Jinja2 that are unused -# in the module but are exported as public interface. -from werkzeug import abort, redirect -from jinja2 import Markup, escape - -# use pkg_resource if that works, otherwise fall back to cwd. The -# current working directory is generally not reliable with the notable -# exception of google appengine. -try: - import pkg_resources - pkg_resources.resource_stream -except (ImportError, AttributeError): - pkg_resources = None - -# a lock used for logger initialization -_logger_lock = Lock() - - -class Request(RequestBase): - """The request object used by default in flask. Remembers the - matched endpoint and view arguments. - - It is what ends up as :class:`~flask.request`. If you want to replace - the request object used you can subclass this and set - :attr:`~flask.Flask.request_class` to your subclass. - """ - - #: the endpoint that matched the request. This in combination with - #: :attr:`view_args` can be used to reconstruct the same or a - #: modified URL. If an exception happened when matching, this will - #: be `None`. - endpoint = None - - #: a dict of view arguments that matched the request. If an exception - #: happened when matching, this will be `None`. - view_args = None - - #: if matching the URL failed, this is the exception that will be - #: raised / was raised as part of the request handling. This is - #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or - #: something similar. - routing_exception = None - - @property - def module(self): - """The name of the current module""" - if self.endpoint and '.' in self.endpoint: - return self.endpoint.rsplit('.', 1)[0] - - @cached_property - def json(self): - """If the mimetype is `application/json` this will contain the - parsed JSON data. - """ - if __debug__: - _assert_have_json() - if self.mimetype == 'application/json': - return json.loads(self.data) - - -class Response(ResponseBase): - """The response object that is used by default in flask. Works like the - response object from Werkzeug but is set to have a HTML mimetype by - default. Quite often you don't have to create this object yourself because - :meth:`~flask.Flask.make_response` will take care of that for you. - - If you want to replace the response object used you can subclass this and - set :attr:`~flask.Flask.response_class` to your subclass. - """ - default_mimetype = 'text/html' - - -class _RequestGlobals(object): - pass - - -class Session(SecureCookie): - """Expands the session with support for switching between permanent - and non-permanent sessions. - """ - - def _get_permanent(self): - return self.get('_permanent', False) - - def _set_permanent(self, value): - self['_permanent'] = bool(value) - - permanent = property(_get_permanent, _set_permanent) - del _get_permanent, _set_permanent - - -class _NullSession(Session): - """Class used to generate nicer error messages if sessions are not - available. Will still allow read-only access to the empty session - but fail on setting. - """ - - def _fail(self, *args, **kwargs): - raise RuntimeError('the session is unavailable because no secret ' - 'key was set. Set the secret_key on the ' - 'application to something unique and secret') - __setitem__ = __delitem__ = clear = pop = popitem = \ - update = setdefault = _fail - del _fail - - -class _RequestContext(object): - """The request context contains all request relevant information. It is - created at the beginning of the request and pushed to the - `_request_ctx_stack` and removed at the end of it. It will create the - URL adapter and request object for the WSGI environment provided. - """ - - def __init__(self, app, environ): - self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ, - server_name=app.config['SERVER_NAME']) - self.request = app.request_class(environ) - self.session = app.open_session(self.request) - if self.session is None: - self.session = _NullSession() - self.g = _RequestGlobals() - self.flashes = None - - try: - self.request.endpoint, self.request.view_args = \ - self.url_adapter.match() - except HTTPException, e: - self.request.routing_exception = e - - def push(self): - """Binds the request context.""" - _request_ctx_stack.push(self) - - def pop(self): - """Pops the request context.""" - _request_ctx_stack.pop() - - def __enter__(self): - self.push() - return self - - def __exit__(self, exc_type, exc_value, tb): - # do not pop the request stack if we are in debug mode and an - # exception happened. This will allow the debugger to still - # access the request object in the interactive shell. Furthermore - # the context can be force kept alive for the test client. - if not self.request.environ.get('flask._preserve_context') and \ - (tb is None or not self.app.debug): - self.pop() - - -def url_for(endpoint, **values): - """Generates a URL to the given endpoint with the method provided. - The endpoint is relative to the active module if modules are in use. - - Here some examples: - - ==================== ======================= ============================= - Active Module Target Endpoint Target Function - ==================== ======================= ============================= - `None` ``'index'`` `index` of the application - `None` ``'.index'`` `index` of the application - ``'admin'`` ``'index'`` `index` of the `admin` module - any ``'.index'`` `index` of the application - any ``'admin.index'`` `index` of the `admin` module - ==================== ======================= ============================= - - Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. - - For more information, head over to the :ref:`Quickstart `. - - :param endpoint: the endpoint of the URL (name of the function) - :param values: the variable arguments of the URL rule - :param _external: if set to `True`, an absolute URL is generated. - """ - ctx = _request_ctx_stack.top - if '.' not in endpoint: - mod = ctx.request.module - if mod is not None: - endpoint = mod + '.' + endpoint - elif endpoint.startswith('.'): - endpoint = endpoint[1:] - external = values.pop('_external', False) - return ctx.url_adapter.build(endpoint, values, force_external=external) - - -def get_template_attribute(template_name, attribute): - """Loads a macro (or variable) a template exports. This can be used to - invoke a macro from within Python code. If you for example have a - template named `_cider.html` with the following contents: - - .. sourcecode:: html+jinja - - {% macro hello(name) %}Hello {{ name }}!{% endmacro %} - - You can access this from Python code like this:: - - hello = get_template_attribute('_cider.html', 'hello') - return hello('World') - - .. versionadded:: 0.2 - - :param template_name: the name of the template - :param attribute: the name of the variable of macro to acccess - """ - return getattr(current_app.jinja_env.get_template(template_name).module, - attribute) - - -def flash(message, category='message'): - """Flashes a message to the next request. In order to remove the - flashed message from the session and to display it to the user, - the template has to call :func:`get_flashed_messages`. - - .. versionchanged: 0.3 - `category` parameter added. - - :param message: the message to be flashed. - :param category: the category for the message. The following values - are recommended: ``'message'`` for any kind of message, - ``'error'`` for errors, ``'info'`` for information - messages and ``'warning'`` for warnings. However any - kind of string can be used as category. - """ - session.setdefault('_flashes', []).append((category, message)) - - -def get_flashed_messages(with_categories=False): - """Pulls all flashed messages from the session and returns them. - Further calls in the same request to the function will return - the same messages. By default just the messages are returned, - but when `with_categories` is set to `True`, the return value will - be a list of tuples in the form ``(category, message)`` instead. - - Example usage: - - .. sourcecode:: html+jinja - - {% for category, msg in get_flashed_messages(with_categories=true) %} -

    {{ msg }} - {% endfor %} - - .. versionchanged:: 0.3 - `with_categories` parameter added. - - :param with_categories: set to `True` to also receive categories. - """ - flashes = _request_ctx_stack.top.flashes - if flashes is None: - _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) - if not with_categories: - return [x[1] for x in flashes] - return flashes - - -def jsonify(*args, **kwargs): - """Creates a :class:`~flask.Response` with the JSON representation of - the given arguments with an `application/json` mimetype. The arguments - to this function are the same as to the :class:`dict` constructor. - - Example usage:: - - @app.route('/_get_current_user') - def get_current_user(): - return jsonify(username=g.user.username, - email=g.user.email, - id=g.user.id) - - This will send a JSON response like this to the browser:: - - { - "username": "admin", - "email": "admin@localhost", - "id": 42 - } - - This requires Python 2.6 or an installed version of simplejson. For - security reasons only objects are supported toplevel. For more - information about this, have a look at :ref:`json-security`. - - .. versionadded:: 0.2 - """ - if __debug__: - _assert_have_json() - return current_app.response_class(json.dumps(dict(*args, **kwargs), - indent=None if request.is_xhr else 2), mimetype='application/json') - - -def send_file(filename_or_fp, mimetype=None, as_attachment=False, - attachment_filename=None): - """Sends the contents of a file to the client. This will use the - most efficient method available and configured. By default it will - try to use the WSGI server's file_wrapper support. Alternatively - you can set the application's :attr:`~Flask.use_x_sendfile` attribute - to ``True`` to directly emit an `X-Sendfile` header. This however - requires support of the underlying webserver for `X-Sendfile`. - - By default it will try to guess the mimetype for you, but you can - also explicitly provide one. For extra security you probably want - to sent certain files as attachment (HTML for instance). - - Please never pass filenames to this function from user sources without - checking them first. Something like this is usually sufficient to - avoid security problems:: - - if '..' in filename or filename.startswith('/'): - abort(404) - - .. versionadded:: 0.2 - - :param filename_or_fp: the filename of the file to send. This is - relative to the :attr:`~Flask.root_path` if a - relative path is specified. - Alternatively a file object might be provided - in which case `X-Sendfile` might not work and - fall back to the traditional method. - :param mimetype: the mimetype of the file if provided, otherwise - auto detection happens. - :param as_attachment: set to `True` if you want to send this file with - a ``Content-Disposition: attachment`` header. - :param attachment_filename: the filename for the attachment if it - differs from the file's filename. - """ - if isinstance(filename_or_fp, basestring): - filename = filename_or_fp - file = None - else: - file = filename_or_fp - filename = getattr(file, 'name', None) - if filename is not None: - filename = os.path.join(current_app.root_path, filename) - if mimetype is None and (filename or attachment_filename): - mimetype = mimetypes.guess_type(filename or attachment_filename)[0] - if mimetype is None: - mimetype = 'application/octet-stream' - - headers = Headers() - if as_attachment: - if attachment_filename is None: - if filename is None: - raise TypeError('filename unavailable, required for ' - 'sending as attachment') - attachment_filename = os.path.basename(filename) - headers.add('Content-Disposition', 'attachment', - filename=attachment_filename) - - if current_app.use_x_sendfile and filename: - if file is not None: - file.close() - headers['X-Sendfile'] = filename - data = None - else: - if file is None: - file = open(filename, 'rb') - data = wrap_file(request.environ, file) - - return Response(data, mimetype=mimetype, headers=headers, - direct_passthrough=True) - - -def render_template(template_name, **context): - """Renders a template from the template folder with the given - context. - - :param template_name: the name of the template to be rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.get_template(template_name).render(context) - - -def render_template_string(source, **context): - """Renders a template from the given template source string - with the given context. - - :param template_name: the sourcecode of the template to be - rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.from_string(source).render(context) - - -def _default_template_ctx_processor(): - """Default template context processor. Injects `request`, - `session` and `g`. - """ - reqctx = _request_ctx_stack.top - return dict( - request=reqctx.request, - session=reqctx.session, - g=reqctx.g - ) - - -def _assert_have_json(): - """Helper function that fails if JSON is unavailable.""" - if not json_available: - raise RuntimeError('simplejson not installed') - - -def _get_package_path(name): - """Returns the path to a package or cwd if that cannot be found.""" - try: - return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) - except (KeyError, AttributeError): - return os.getcwd() - - -# figure out if simplejson escapes slashes. This behaviour was changed -# from one version to another without reason. -if not json_available or '\\/' not in json.dumps('/'): - - def _tojson_filter(*args, **kwargs): - if __debug__: - _assert_have_json() - return json.dumps(*args, **kwargs).replace('/', '\\/') -else: - _tojson_filter = json.dumps - - -class _PackageBoundObject(object): - - def __init__(self, import_name): - #: The name of the package or module. Do not change this once - #: it was set by the constructor. - self.import_name = import_name - - #: Where is the app root located? - self.root_path = _get_package_path(self.import_name) - - def open_resource(self, resource): - """Opens a resource from the application's resource folder. To see - how this works, consider the following folder structure:: - - /myapplication.py - /schemal.sql - /static - /style.css - /templates - /layout.html - /index.html - - If you want to open the `schema.sql` file you would do the - following:: - - with app.open_resource('schema.sql') as f: - contents = f.read() - do_something_with(contents) - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - """ - if pkg_resources is None: - return open(os.path.join(self.root_path, resource), 'rb') - return pkg_resources.resource_stream(self.import_name, resource) - - -class _ModuleSetupState(object): - - def __init__(self, app, url_prefix=None): - self.app = app - self.url_prefix = url_prefix - - -class Module(_PackageBoundObject): - """Container object that enables pluggable applications. A module can - be used to organize larger applications. They represent blueprints that, - in combination with a :class:`Flask` object are used to create a large - application. - - A module is like an application bound to an `import_name`. Multiple - modules can share the same import names, but in that case a `name` has - to be provided to keep them apart. If different import names are used, - the rightmost part of the import name is used as name. - - Here an example structure for a larger appliation:: - - /myapplication - /__init__.py - /views - /__init__.py - /admin.py - /frontend.py - - The `myapplication/__init__.py` can look like this:: - - from flask import Flask - from myapplication.views.admin import admin - from myapplication.views.frontend import frontend - - app = Flask(__name__) - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - - And here an example view module (`myapplication/views/admin.py`):: - - from flask import Module - - admin = Module(__name__) - - @admin.route('/') - def index(): - pass - - @admin.route('/login') - def login(): - pass - - For a gentle introduction into modules, checkout the - :ref:`working-with-modules` section. - """ - - def __init__(self, import_name, name=None, url_prefix=None): - if name is None: - assert '.' in import_name, 'name required if package name ' \ - 'does not point to a submodule' - name = import_name.rsplit('.', 1)[1] - _PackageBoundObject.__init__(self, import_name) - self.name = name - self.url_prefix = url_prefix - self._register_events = [] - - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a module. The endpoint for the - :func:`url_for` function is prefixed with the name of the module. - """ - def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) - return f - return decorator - - def add_url_rule(self, rule, endpoint, view_func=None, **options): - """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for - the :func:`url_for` function is prefixed with the name of the module. - """ - def register_rule(state): - the_rule = rule - if state.url_prefix: - the_rule = state.url_prefix + rule - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), - view_func, **options) - self._record(register_rule) - - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a module. This function - is only executed before each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def before_app_request(self, f): - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(None, []).append(f)) - return f - - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a module. This function - is only executed after each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def after_app_request(self, f): - """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request, even if outside of the module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(None, []).append(f)) - return f - - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. This - function is only executed for requests handled by a module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(self.name, []).append(f)) - return f - - def app_context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. Such a - function is executed each request, even if outside of the module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(None, []).append(f)) - return f - - def app_errorhandler(self, code): - """Like :meth:`Flask.errorhandler` but for a module. This - handler is used for all requests, even if outside of the module. - - .. versionadded:: 0.4 - """ - def decorator(f): - self._record(lambda s: s.app.errorhandler(code)(f)) - return f - return decorator - - def _record(self, func): - self._register_events.append(func) - - -class ConfigAttribute(object): - """Makes an attribute forward to the config""" - - def __init__(self, name): - self.__name__ = name - - def __get__(self, obj, type=None): - if obj is None: - return self - return obj.config[self.__name__] - - def __set__(self, obj, value): - obj.config[self.__name__] = value - - -class Config(dict): - """Works exactly like a dict but provides ways to fill it from files - or special dictionaries. There are two common patterns to populate the - config. - - Either you can fill the config from a config file:: - - app.config.from_pyfile('yourconfig.cfg') - - Or alternatively you can define the configuration options in the - module that calls :meth:`from_object` or provide an import path to - a module that should be loaded. It is also possible to tell it to - use the same module and with that provide the configuration values - just before the call:: - - DEBUG = True - SECRET_KEY = 'development key' - app.config.from_object(__name__) - - In both cases (loading from any Python file or loading from modules), - only uppercase keys are added to the config. This makes it possible to use - lowercase values in the config file for temporary values that are not added - to the config or to define the config keys in the same file that implements - the application. - - Probably the most interesting way to load configurations is from an - environment variable pointing to a file:: - - app.config.from_envvar('YOURAPPLICATION_SETTINGS') - - In this case before launching the application you have to set this - environment variable to the file you want to use. On Linux and OS X - use the export statement:: - - export YOURAPPLICATION_SETTINGS='/path/to/config/file' - - On windows use `set` instead. - - :param root_path: path to which files are read relative from. When the - config object is created by the application, this is - the application's :attr:`~flask.Flask.root_path`. - :param defaults: an optional dictionary of default values - """ - - def __init__(self, root_path, defaults=None): - dict.__init__(self, defaults or {}) - self.root_path = root_path - - def from_envvar(self, variable_name, silent=False): - """Loads a configuration from an environment variable pointing to - a configuration file. This basically is just a shortcut with nicer - error messages for this line of code:: - - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) - - :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent failing for missing - files. - :return: bool. `True` if able to load config, `False` otherwise. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError('The environment variable %r is not set ' - 'and as such configuration could not be ' - 'loaded. Set this variable and make it ' - 'point to a configuration file' % - variable_name) - self.from_pyfile(rv) - return True - - def from_pyfile(self, filename): - """Updates the values in the config from a Python file. This function - behaves as if the file was imported as module with the - :meth:`from_object` function. - - :param filename: the filename of the config. This can either be an - absolute filename or a filename relative to the - root path. - """ - filename = os.path.join(self.root_path, filename) - d = type(sys)('config') - d.__file__ = filename - execfile(filename, d.__dict__) - self.from_object(d) - - def from_object(self, obj): - """Updates the values from the given object. An object can be of one - of the following two types: - - - a string: in this case the object with that name will be imported - - an actual object reference: that object is used directly - - Objects are usually either modules or classes. - - Just the uppercase variables in that object are stored in the config - after lowercasing. Example usage:: - - app.config.from_object('yourapplication.default_config') - from yourapplication import default_config - app.config.from_object(default_config) - - You should not use this function to load the actual configuration but - rather configuration defaults. The actual config should be loaded - with :meth:`from_pyfile` and ideally from a location not within the - package because the package might be installed system wide. - - :param obj: an import name or object - """ - if isinstance(obj, basestring): - obj = import_string(obj) - for key in dir(obj): - if key.isupper(): - self[key] = getattr(obj, key) - - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) - - -class Flask(_PackageBoundObject): - """The flask object implements a WSGI application and acts as the central - object. It is passed the name of the module or package of the - application. Once it is created it will act as a central registry for - the view functions, the URL rules, template configuration and much more. - - The name of the package is used to resolve resources from inside the - package or the folder the module is contained in depending on if the - package parameter resolves to an actual python package (a folder with - an `__init__.py` file inside) or a standard module (just a `.py` file). - - For more information about resource loading, see :func:`open_resource`. - - Usually you create a :class:`Flask` instance in your main module or - in the `__init__.py` file of your package like this:: - - from flask import Flask - app = Flask(__name__) - - .. admonition:: About the First Parameter - - The idea of the first parameter is to give Flask an idea what - belongs to your application. This name is used to find resources - on the file system, can be used by extensions to improve debugging - information and a lot more. - - So it's important what you provide there. If you are using a single - module, `__name__` is always the correct value. If you however are - using a package, it's usually recommended to hardcode the name of - your package there. - - For example if your application is defined in `yourapplication/app.py` - you should create it with one of the two versions below:: - - app = Flask('yourapplication') - app = Flask(__name__.split('.')[0]) - - Why is that? The application will work even with `__name__`, thanks - to how resources are looked up. However it will make debugging more - painful. Certain extensions can make assumptions based on the - import name of your application. For example the Flask-SQLAlchemy - extension will look for the code in your application that triggered - an SQL query in debug mode. If the import name is not properly set - up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplicaiton.app` and not - `yourapplication.views.frontend`) - """ - - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class = Response - - #: Path for the static files. If you don't want to use static files - #: you can set this value to `None` in which case no URL rule is added - #: and the development server will no longer serve any static files. - static_path = '/static' - - #: The debug flag. Set this to `True` to enable debugging of the - #: application. In debug mode the debugger will kick in when an unhandled - #: exception ocurrs and the integrated server will automatically reload - #: the application if changes in the code are detected. - #: - #: This attribute can also be configured from the config with the `DEBUG` - #: configuration key. Defaults to `False`. - debug = ConfigAttribute('DEBUG') - - #: The testing flask. Set this to `True` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate unittest helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: This attribute can also be configured from the config with the - #: `TESTING` configuration key. Defaults to `False`. - testing = ConfigAttribute('TESTING') - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: `SECRET_KEY` configuration key. Defaults to `None`. - secret_key = ConfigAttribute('SECRET_KEY') - - #: The secure cookie uses this for the name of the session cookie. - #: - #: This attribute can also be configured from the config with the - #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` - session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') - - #: Enable this if you want to use the X-Sendfile feature. Keep in - #: mind that the server has to support this. This only affects files - #: sent with the :func:`send_file` method. - #: - #: .. versionadded:: 0.2 - #: - #: This attribute can also be configured from the config with the - #: `USE_X_SENDFILE` configuration key. Defaults to `False`. - use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') - - #: The name of the logger to use. By default the logger name is the - #: package name passed to the constructor. - #: - #: .. versionadded:: 0.4 - logger_name = ConfigAttribute('LOGGER_NAME') - - #: The logging format used for the debug logger. This is only used when - #: the application is in debug mode, otherwise the attached logging - #: handler does the formatting. - #: - #: .. versionadded:: 0.3 - debug_log_format = ( - '-' * 80 + '\n' + - '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + - '%(message)s\n' + - '-' * 80 - ) - - #: Options that are passed directly to the Jinja2 environment. - jinja_options = ImmutableDict( - autoescape=True, - extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] - ) - - #: Default configuration parameters. - default_config = ImmutableDict({ - 'DEBUG': False, - 'TESTING': False, - 'SECRET_KEY': None, - 'SESSION_COOKIE_NAME': 'session', - 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), - 'USE_X_SENDFILE': False, - 'LOGGER_NAME': None, - 'SERVER_NAME': None - }) - - def __init__(self, import_name): - _PackageBoundObject.__init__(self, import_name) - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = Config(self.root_path, self.default_config) - - #: Prepare the deferred setup of the logger. - self._logger = None - self.logger_name = self.import_name - - #: A dictionary of all view functions registered. The keys will - #: be function names which are also used to generate URLs and - #: the values are the function objects themselves. - #: to register a view function, use the :meth:`route` decorator. - self.view_functions = {} - - #: A dictionary of all registered error handlers. The key is - #: be the error code as integer, the value the function that - #: should handle that error. - #: To register a error handler, use the :meth:`errorhandler` - #: decorator. - self.error_handlers = {} - - #: A dictionary with lists of functions that should be called at the - #: beginning of the request. The key of the dictionary is the name of - #: the module this function is active for, `None` for all requests. - #: This can for example be used to open database connections or - #: getting hold of the currently logged in user. To register a - #: function here, use the :meth:`before_request` decorator. - self.before_request_funcs = {} - - #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the module - #: this function is active for, `None` for all requests. This can for - #: example be used to open database connections or getting hold of the - #: currently logged in user. To register a function here, use the - #: :meth:`before_request` decorator. - self.after_request_funcs = {} - - #: A dictionary with list of functions that are called without argument - #: to populate the template context. They key of the dictionary is the - #: name of the module this function is active for, `None` for all - #: requests. Each returns a dictionary that the template context is - #: updated with. To register a function here, use the - #: :meth:`context_processor` decorator. - self.template_context_processors = { - None: [_default_template_ctx_processor] - } - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(BaseConverter.to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = Map() - - if self.static_path is not None: - self.add_url_rule(self.static_path + '/', - build_only=True, endpoint='static') - if pkg_resources is not None: - target = (self.import_name, 'static') - else: - target = os.path.join(self.root_path, 'static') - self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { - self.static_path: target - }) - - #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options` and the loader that is returned - #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter - - @property - def logger(self): - """A :class:`logging.Logger` object for this application. The - default configuration is to log to stderr if the application is - in debug mode. This logger can be used to (surprise) log messages. - Here some examples:: - - app.logger.debug('A value for debugging') - app.logger.warning('A warning ocurred (%d apples)', 42) - app.logger.error('An error occoured') - - .. versionadded:: 0.3 - """ - if self._logger and self._logger.name == self.logger_name: - return self._logger - with _logger_lock: - if self._logger and self._logger.name == self.logger_name: - return self._logger - from logging import getLogger, StreamHandler, Formatter, \ - Logger, DEBUG - class DebugLogger(Logger): - def getEffectiveLevel(x): - return DEBUG if self.debug else Logger.getEffectiveLevel(x) - class DebugHandler(StreamHandler): - def emit(x, record): - StreamHandler.emit(x, record) if self.debug else None - handler = DebugHandler() - handler.setLevel(DEBUG) - handler.setFormatter(Formatter(self.debug_log_format)) - logger = getLogger(self.logger_name) - logger.__class__ = DebugLogger - logger.addHandler(handler) - self._logger = logger - return logger - - def create_jinja_loader(self): - """Creates the Jinja loader. By default just a package loader for - the configured package is returned that looks up templates in the - `templates` folder. To add other loaders it's possible to - override this method. - """ - if pkg_resources is None: - return FileSystemLoader(os.path.join(self.root_path, 'templates')) - return PackageLoader(self.import_name) - - def update_template_context(self, context): - """Update the template context with some commonly used variables. - This injects request, session and g into the template context. - - :param context: the context as a dictionary that is updated in place - to add extra variables. - """ - funcs = self.template_context_processors[None] - mod = _request_ctx_stack.top.request.module - if mod is not None and mod in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[mod]) - for func in funcs: - context.update(func()) - - def run(self, host='127.0.0.1', port=5000, **options): - """Runs the application on a local development server. If the - :attr:`debug` flag is set the server will automatically reload - for code changes and show a debugger in case an exception happened. - - .. admonition:: Keep in Mind - - Flask will supress any server error with a generic error page - unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you ahve to - invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. - Setting ``use_debugger`` to `True` without being in debug mode - won't catch any exceptions because there won't be any to - catch. - - :param host: the hostname to listen on. set this to ``'0.0.0.0'`` - to have the server available externally as well. - :param port: the port of the webserver - :param options: the options to be forwarded to the underlying - Werkzeug server. See :func:`werkzeug.run_simple` - for more information. - """ - from werkzeug import run_simple - if 'debug' in options: - self.debug = options.pop('debug') - options.setdefault('use_reloader', self.debug) - options.setdefault('use_debugger', self.debug) - return run_simple(host, port, self, **options) - - def test_client(self): - """Creates a test client for this application. For information - about unit testing head over to :ref:`testing`. - - The test client can be used in a `with` block to defer the closing down - of the context until the end of the `with` block. This is useful if - you want to access the context locals for testing:: - - with app.test_client() as c: - rv = c.get('/?vodka=42') - assert request.args['vodka'] == '42' - - .. versionchanged:: 0.4 - added support for `with` block usage for the client. - """ - from werkzeug import Client - class FlaskClient(Client): - preserve_context = context_preserved = False - def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False - kwargs.setdefault('environ_overrides', {}) \ - ['flask._preserve_context'] = self.preserve_context - old = _request_ctx_stack.top - try: - return Client.open(self, *args, **kwargs) - finally: - self.context_preserved = _request_ctx_stack.top is not old - def __enter__(self): - self.preserve_context = True - return self - def __exit__(self, exc_type, exc_value, tb): - self.preserve_context = False - if self.context_preserved: - _request_ctx_stack.pop() - return FlaskClient(self, self.response_class, use_cookies=True) - - def open_session(self, request): - """Creates or opens a new session. Default implementation stores all - session data in a signed cookie. This requires that the - :attr:`secret_key` is set. - - :param request: an instance of :attr:`request_class`. - """ - key = self.secret_key - if key is not None: - return Session.load_cookie(request, self.session_cookie_name, - secret_key=key) - - def save_session(self, session, response): - """Saves the session if it needs updates. For the default - implementation, check :meth:`open_session`. - - :param session: the session to be saved (a - :class:`~werkzeug.contrib.securecookie.SecureCookie` - object) - :param response: an instance of :attr:`response_class` - """ - expires = None - if session.permanent: - expires = datetime.utcnow() + self.permanent_session_lifetime - session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True) - - def register_module(self, module, **options): - """Registers a module with this application. The keyword argument - of this function are the same as the ones for the constructor of the - :class:`Module` class and will override the values of the module if - provided. - """ - options.setdefault('url_prefix', module.url_prefix) - state = _ModuleSetupState(self, **options) - for func in module._register_events: - func(state) - - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): - """Connects a URL rule. Works exactly like the :meth:`route` - decorator. If a view_func is provided it will be registered with the - endpoint. - - Basically this example:: - - @app.route('/') - def index(): - pass - - Is equivalent to the following:: - - def index(): - pass - app.add_url_rule('/', 'index', index) - - If the view_func is not provided you will need to connect the endpoint - to a view function like so:: - - app.view_functions['index'] = index - - .. versionchanged:: 0.2 - `view_func` parameter added. - - :param rule: the URL rule as string - :param endpoint: the endpoint for the registered URL rule. Flask - itself assumes the name of the view function as - endpoint - :param view_func: the function to call when serving a request to the - provided endpoint - :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object - """ - if endpoint is None: - assert view_func is not None, 'expected view func if endpoint ' \ - 'is not provided.' - endpoint = view_func.__name__ - options['endpoint'] = endpoint - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) - if view_func is not None: - self.view_functions[endpoint] = view_func - - def route(self, rule, **options): - """A decorator that is used to register a view function for a - given URL rule. Example:: - - @app.route('/') - def index(): - return 'Hello World' - - Variables parts in the route can be specified with angular - brackets (``/user/``). By default a variable part - in the URL accepts any string without a slash however a different - converter can be specified as well by using ````. - - Variable parts are passed to the view function as keyword - arguments. - - The following converters are possible: - - =========== =========================================== - `int` accepts integers - `float` like `int` but for floating point values - `path` like the default but also accepts slashes - =========== =========================================== - - Here some examples:: - - @app.route('/') - def index(): - pass - - @app.route('/') - def show_user(username): - pass - - @app.route('/post/') - def show_post(post_id): - pass - - An important detail to keep in mind is how Flask deals with trailing - slashes. The idea is to keep each URL unique so the following rules - apply: - - 1. If a rule ends with a slash and is requested without a slash - by the user, the user is automatically redirected to the same - page with a trailing slash attached. - 2. If a rule does not end with a trailing slash and the user request - the page with a trailing slash, a 404 not found is raised. - - This is consistent with how web servers deal with static files. This - also makes it possible to use relative link targets safely. - - The :meth:`route` decorator accepts a couple of other arguments - as well: - - :param rule: the URL rule as string - :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case - subdomain matching is in use. - :param strict_slashes: can be used to disable the strict slashes - setting for this rule. See above. - :param options: other options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. - """ - def decorator(f): - self.add_url_rule(rule, None, f, **options) - return f - return decorator - - def errorhandler(self, code): - """A decorator that is used to register a function give a given - error code. Example:: - - @app.errorhandler(404) - def page_not_found(error): - return 'This page does not exist', 404 - - You can also register a function as error handler without using - the :meth:`errorhandler` decorator. The following example is - equivalent to the one above:: - - def page_not_found(error): - return 'This page does not exist', 404 - app.error_handlers[404] = page_not_found - - :param code: the code as integer for the handler - """ - def decorator(f): - self.error_handlers[code] = f - return f - return decorator - - def template_filter(self, name=None): - """A decorator that is used to register custom template filter. - You can specify a name for the filter, otherwise the function - name will be used. Example:: - - @app.template_filter() - def reverse(s): - return s[::-1] - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - def decorator(f): - self.jinja_env.filters[name or f.__name__] = f - return f - return decorator - - def before_request(self, f): - """Registers a function to run before each request.""" - self.before_request_funcs.setdefault(None, []).append(f) - return f - - def after_request(self, f): - """Register a function to be run after each request.""" - self.after_request_funcs.setdefault(None, []).append(f) - return f - - def context_processor(self, f): - """Registers a template context processor function.""" - self.template_context_processors[None].append(f) - return f - - def handle_http_exception(self, e): - """Handles an HTTP exception. By default this will invoke the - registered error handlers and fall back to returning the - exception as response. - - .. versionadded: 0.3 - """ - handler = self.error_handlers.get(e.code) - if handler is None: - return e - return handler(e) - - def handle_exception(self, e): - """Default exception handling that kicks in when an exception - occours that is not catched. In debug mode the exception will - be re-raised immediately, otherwise it is logged and the handler - for a 500 internal server error is used. If no such handler - exists, a default 500 internal server error message is displayed. - - .. versionadded: 0.3 - """ - handler = self.error_handlers.get(500) - if self.debug: - raise - self.logger.exception('Exception on %s [%s]' % ( - request.path, - request.method - )) - if handler is None: - return InternalServerError() - return handler(e) - - def dispatch_request(self): - """Does the request dispatching. Matches the URL and returns the - return value of the view or error handler. This does not have to - be a response object. In order to convert the return value to a - proper response object, call :func:`make_response`. - """ - req = _request_ctx_stack.top.request - try: - if req.routing_exception is not None: - raise req.routing_exception - return self.view_functions[req.endpoint](**req.view_args) - except HTTPException, e: - return self.handle_http_exception(e) - - def make_response(self, rv): - """Converts the return value from a view function to a real - response object that is an instance of :attr:`response_class`. - - The following types are allowed for `rv`: - - .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| - - ======================= =========================================== - :attr:`response_class` the object is returned unchanged - :class:`str` a response object is created with the - string as body - :class:`unicode` a response object is created with the - string encoded to utf-8 as body - :class:`tuple` the response object is created with the - contents of the tuple as arguments - a WSGI function the function is called as WSGI application - and buffered as response object - ======================= =========================================== - - :param rv: the return value from the view function - """ - if rv is None: - raise ValueError('View function did not return a response') - if isinstance(rv, self.response_class): - return rv - if isinstance(rv, basestring): - return self.response_class(rv) - if isinstance(rv, tuple): - return self.response_class(*rv) - return self.response_class.force_type(rv, request.environ) - - def preprocess_request(self): - """Called before the actual request dispatching and will - call every as :meth:`before_request` decorated function. - If any of these function returns a value it's handled as - if it was the return value from the view and further - request handling is stopped. - """ - funcs = self.before_request_funcs.get(None, ()) - mod = request.module - if mod and mod in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[mod]) - for func in funcs: - rv = func() - if rv is not None: - return rv - - def process_response(self, response): - """Can be overridden in order to modify the response object - before it's sent to the WSGI server. By default this will - call all the :meth:`after_request` decorated functions. - - :param response: a :attr:`response_class` object. - :return: a new response object or the same, has to be an - instance of :attr:`response_class`. - """ - ctx = _request_ctx_stack.top - mod = ctx.request.module - if not isinstance(ctx.session, _NullSession): - self.save_session(ctx.session, response) - funcs = () - if mod and mod in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[mod]) - if None in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[None]) - for handler in funcs: - response = handler(response) - return response - - def wsgi_app(self, environ, start_response): - """The actual WSGI application. This is not implemented in - `__call__` so that middlewares can be applied without losing a - reference to the class. So instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - - :param environ: a WSGI environment - :param start_response: a callable accepting a status code, - a list of headers and an optional - exception context to start the response - """ - with self.request_context(environ): - try: - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - return response(environ, start_response) - - def request_context(self, environ): - """Creates a request context from the given environment and binds - it to the current context. This must be used in combination with - the `with` statement because the request is only bound to the - current context for the duration of the `with` block. - - Example usage:: - - with app.request_context(environ): - do_something_with(request) - - The object returned can also be used without the `with` statement - which is useful for working in the shell. The example above is - doing exactly the same as this code:: - - ctx = app.request_context(environ) - ctx.push() - try: - do_something_with(request) - finally: - ctx.pop() - - The big advantage of this approach is that you can use it without - the try/finally statement in a shell for interactive testing: - - >>> ctx = app.test_request_context() - >>> ctx.bind() - >>> request.path - u'/' - >>> ctx.unbind() - - .. versionchanged:: 0.3 - Added support for non-with statement usage and `with` statement - is now passed the ctx object. - - :param environ: a WSGI environment - """ - return _RequestContext(self, environ) - - def test_request_context(self, *args, **kwargs): - """Creates a WSGI environment from the given values (see - :func:`werkzeug.create_environ` for more information, this - function accepts the same arguments). - """ - return self.request_context(create_environ(*args, **kwargs)) - - def __call__(self, environ, start_response): - """Shortcut for :attr:`wsgi_app`.""" - return self.wsgi_app(environ, start_response) - - -# context locals -_request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) From d0dc89ea802130e8a3a16b4fba73fa10815c09fb Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 14:20:58 -0400 Subject: [PATCH 0316/3747] in with the new. i have the bits in places where i think they should be, now i just need to work on the import scheme layout --- flask/__init__.py | 58 ++++ flask/app.py | 778 ++++++++++++++++++++++++++++++++++++++++++++++ flask/conf.py | 135 ++++++++ flask/ctx.py | 57 ++++ flask/helpers.py | 310 ++++++++++++++++++ flask/module.py | 151 +++++++++ flask/session.py | 28 ++ flask/wrappers.py | 57 ++++ 8 files changed, 1574 insertions(+) create mode 100644 flask/__init__.py create mode 100644 flask/app.py create mode 100644 flask/conf.py create mode 100644 flask/ctx.py create mode 100644 flask/helpers.py create mode 100644 flask/module.py create mode 100644 flask/session.py create mode 100644 flask/wrappers.py diff --git a/flask/__init__.py b/flask/__init__.py new file mode 100644 index 00000000..f7ae2551 --- /dev/null +++ b/flask/__init__.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" + flask + ~~~~~ + + A microframework based on Werkzeug. It's extensively documented + and follows best practice patterns. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from __future__ import with_statement +import os +import sys +import mimetypes +from datetime import datetime, timedelta + +# this is a workaround for appengine. Do not remove this import +import werkzeug + +from itertools import chain +from threading import Lock +from jinja2 import Environment, PackageLoader, FileSystemLoader +from werkzeug import Request as RequestBase, Response as ResponseBase, \ + LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ + ImmutableDict, cached_property, wrap_file, Headers, \ + import_string +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.contrib.securecookie import SecureCookie + + + +# utilities we import from Werkzeug and Jinja2 that are unused +# in the module but are exported as public interface. +from werkzeug import abort, redirect +from jinja2 import Markup, escape + +# use pkg_resource if that works, otherwise fall back to cwd. The +# current working directory is generally not reliable with the notable +# exception of google appengine. +try: + import pkg_resources + pkg_resources.resource_stream +except (ImportError, AttributeError): + pkg_resources = None + +# a lock used for logger initialization +_logger_lock = Lock() + + + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(lambda: _request_ctx_stack.top.app) +request = LocalProxy(lambda: _request_ctx_stack.top.request) +session = LocalProxy(lambda: _request_ctx_stack.top.session) +g = LocalProxy(lambda: _request_ctx_stack.top.g) diff --git a/flask/app.py b/flask/app.py new file mode 100644 index 00000000..f3e1f73c --- /dev/null +++ b/flask/app.py @@ -0,0 +1,778 @@ + + +class Flask(_PackageBoundObject): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an `__init__.py` file inside) or a standard module (just a `.py` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the `__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea what + belongs to your application. This name is used to find resources + on the file system, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in `yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplicaiton.app` and not + `yourapplication.views.frontend`) + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: Path for the static files. If you don't want to use static files + #: you can set this value to `None` in which case no URL rule is added + #: and the development server will no longer serve any static files. + static_path = '/static' + + #: The debug flag. Set this to `True` to enable debugging of the + #: application. In debug mode the debugger will kick in when an unhandled + #: exception ocurrs and the integrated server will automatically reload + #: the application if changes in the code are detected. + #: + #: This attribute can also be configured from the config with the `DEBUG` + #: configuration key. Defaults to `False`. + debug = ConfigAttribute('DEBUG') + + #: The testing flask. Set this to `True` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate unittest helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: This attribute can also be configured from the config with the + #: `TESTING` configuration key. Defaults to `False`. + testing = ConfigAttribute('TESTING') + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: `SECRET_KEY` configuration key. Defaults to `None`. + secret_key = ConfigAttribute('SECRET_KEY') + + #: The secure cookie uses this for the name of the session cookie. + #: + #: This attribute can also be configured from the config with the + #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` + session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') + + #: Enable this if you want to use the X-Sendfile feature. Keep in + #: mind that the server has to support this. This only affects files + #: sent with the :func:`send_file` method. + #: + #: .. versionadded:: 0.2 + #: + #: This attribute can also be configured from the config with the + #: `USE_X_SENDFILE` configuration key. Defaults to `False`. + use_x_sendfile = ConfigAttribute('USE_X_SENDFILE') + + #: The name of the logger to use. By default the logger name is the + #: package name passed to the constructor. + #: + #: .. versionadded:: 0.4 + logger_name = ConfigAttribute('LOGGER_NAME') + + #: The logging format used for the debug logger. This is only used when + #: the application is in debug mode, otherwise the attached logging + #: handler does the formatting. + #: + #: .. versionadded:: 0.3 + debug_log_format = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + + #: Options that are passed directly to the Jinja2 environment. + jinja_options = ImmutableDict( + autoescape=True, + extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] + ) + + #: Default configuration parameters. + default_config = ImmutableDict({ + 'DEBUG': False, + 'TESTING': False, + 'SECRET_KEY': None, + 'SESSION_COOKIE_NAME': 'session', + 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), + 'USE_X_SENDFILE': False, + 'LOGGER_NAME': None, + 'SERVER_NAME': None + }) + + def __init__(self, import_name): + _PackageBoundObject.__init__(self, import_name) + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = Config(self.root_path, self.default_config) + + #: Prepare the deferred setup of the logger. + self._logger = None + self.logger_name = self.import_name + + #: A dictionary of all view functions registered. The keys will + #: be function names which are also used to generate URLs and + #: the values are the function objects themselves. + #: to register a view function, use the :meth:`route` decorator. + self.view_functions = {} + + #: A dictionary of all registered error handlers. The key is + #: be the error code as integer, the value the function that + #: should handle that error. + #: To register a error handler, use the :meth:`errorhandler` + #: decorator. + self.error_handlers = {} + + #: A dictionary with lists of functions that should be called at the + #: beginning of the request. The key of the dictionary is the name of + #: the module this function is active for, `None` for all requests. + #: This can for example be used to open database connections or + #: getting hold of the currently logged in user. To register a + #: function here, use the :meth:`before_request` decorator. + self.before_request_funcs = {} + + #: A dictionary with lists of functions that should be called after + #: each request. The key of the dictionary is the name of the module + #: this function is active for, `None` for all requests. This can for + #: example be used to open database connections or getting hold of the + #: currently logged in user. To register a function here, use the + #: :meth:`before_request` decorator. + self.after_request_funcs = {} + + #: A dictionary with list of functions that are called without argument + #: to populate the template context. They key of the dictionary is the + #: name of the module this function is active for, `None` for all + #: requests. Each returns a dictionary that the template context is + #: updated with. To register a function here, use the + #: :meth:`context_processor` decorator. + self.template_context_processors = { + None: [_default_template_ctx_processor] + } + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(BaseConverter.to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = Map() + + if self.static_path is not None: + self.add_url_rule(self.static_path + '/', + build_only=True, endpoint='static') + if pkg_resources is not None: + target = (self.import_name, 'static') + else: + target = os.path.join(self.root_path, 'static') + self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { + self.static_path: target + }) + + #: The Jinja2 environment. It is created from the + #: :attr:`jinja_options` and the loader that is returned + #: by the :meth:`create_jinja_loader` function. + self.jinja_env = Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + + @property + def logger(self): + """A :class:`logging.Logger` object for this application. The + default configuration is to log to stderr if the application is + in debug mode. This logger can be used to (surprise) log messages. + Here some examples:: + + app.logger.debug('A value for debugging') + app.logger.warning('A warning ocurred (%d apples)', 42) + app.logger.error('An error occoured') + + .. versionadded:: 0.3 + """ + if self._logger and self._logger.name == self.logger_name: + return self._logger + with _logger_lock: + if self._logger and self._logger.name == self.logger_name: + return self._logger + from logging import getLogger, StreamHandler, Formatter, \ + Logger, DEBUG + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if self.debug else Logger.getEffectiveLevel(x) + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if self.debug else None + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(self.debug_log_format)) + logger = getLogger(self.logger_name) + logger.__class__ = DebugLogger + logger.addHandler(handler) + self._logger = logger + return logger + + def create_jinja_loader(self): + """Creates the Jinja loader. By default just a package loader for + the configured package is returned that looks up templates in the + `templates` folder. To add other loaders it's possible to + override this method. + """ + if pkg_resources is None: + return FileSystemLoader(os.path.join(self.root_path, 'templates')) + return PackageLoader(self.import_name) + + def update_template_context(self, context): + """Update the template context with some commonly used variables. + This injects request, session and g into the template context. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + funcs = self.template_context_processors[None] + mod = _request_ctx_stack.top.request.module + if mod is not None and mod in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[mod]) + for func in funcs: + context.update(func()) + + def run(self, host='127.0.0.1', port=5000, **options): + """Runs the application on a local development server. If the + :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + .. admonition:: Keep in Mind + + Flask will supress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you ahve to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to `True` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. set this to ``'0.0.0.0'`` + to have the server available externally as well. + :param port: the port of the webserver + :param options: the options to be forwarded to the underlying + Werkzeug server. See :func:`werkzeug.run_simple` + for more information. + """ + from werkzeug import run_simple + if 'debug' in options: + self.debug = options.pop('debug') + options.setdefault('use_reloader', self.debug) + options.setdefault('use_debugger', self.debug) + return run_simple(host, port, self, **options) + + def test_client(self): + """Creates a test client for this application. For information + about unit testing head over to :ref:`testing`. + + The test client can be used in a `with` block to defer the closing down + of the context until the end of the `with` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + .. versionchanged:: 0.4 + added support for `with` block usage for the client. + """ + from werkzeug import Client + class FlaskClient(Client): + preserve_context = context_preserved = False + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + def __enter__(self): + self.preserve_context = True + return self + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() + return FlaskClient(self, self.response_class, use_cookies=True) + + def open_session(self, request): + """Creates or opens a new session. Default implementation stores all + session data in a signed cookie. This requires that the + :attr:`secret_key` is set. + + :param request: an instance of :attr:`request_class`. + """ + key = self.secret_key + if key is not None: + return Session.load_cookie(request, self.session_cookie_name, + secret_key=key) + + def save_session(self, session, response): + """Saves the session if it needs updates. For the default + implementation, check :meth:`open_session`. + + :param session: the session to be saved (a + :class:`~werkzeug.contrib.securecookie.SecureCookie` + object) + :param response: an instance of :attr:`response_class` + """ + expires = None + if session.permanent: + expires = datetime.utcnow() + self.permanent_session_lifetime + session.save_cookie(response, self.session_cookie_name, + expires=expires, httponly=True) + + def register_module(self, module, **options): + """Registers a module with this application. The keyword argument + of this function are the same as the ones for the constructor of the + :class:`Module` class and will override the values of the module if + provided. + """ + options.setdefault('url_prefix', module.url_prefix) + state = _ModuleSetupState(self, **options) + for func in module._register_events: + func(state) + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Connects a URL rule. Works exactly like the :meth:`route` + decorator. If a view_func is provided it will be registered with the + endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following:: + + def index(): + pass + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint + to a view function like so:: + + app.view_functions['index'] = index + + .. versionchanged:: 0.2 + `view_func` parameter added. + + :param rule: the URL rule as string + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object + """ + if endpoint is None: + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + endpoint = view_func.__name__ + options['endpoint'] = endpoint + options.setdefault('methods', ('GET',)) + self.url_map.add(Rule(rule, **options)) + if view_func is not None: + self.view_functions[endpoint] = view_func + + def route(self, rule, **options): + """A decorator that is used to register a view function for a + given URL rule. Example:: + + @app.route('/') + def index(): + return 'Hello World' + + Variables parts in the route can be specified with angular + brackets (``/user/``). By default a variable part + in the URL accepts any string without a slash however a different + converter can be specified as well by using ````. + + Variable parts are passed to the view function as keyword + arguments. + + The following converters are possible: + + =========== =========================================== + `int` accepts integers + `float` like `int` but for floating point values + `path` like the default but also accepts slashes + =========== =========================================== + + Here some examples:: + + @app.route('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + pass + + An important detail to keep in mind is how Flask deals with trailing + slashes. The idea is to keep each URL unique so the following rules + apply: + + 1. If a rule ends with a slash and is requested without a slash + by the user, the user is automatically redirected to the same + page with a trailing slash attached. + 2. If a rule does not end with a trailing slash and the user request + the page with a trailing slash, a 404 not found is raised. + + This is consistent with how web servers deal with static files. This + also makes it possible to use relative link targets safely. + + The :meth:`route` decorator accepts a couple of other arguments + as well: + + :param rule: the URL rule as string + :param methods: a list of methods this rule should be limited + to (``GET``, ``POST`` etc.). By default a rule + just listens for ``GET`` (and implicitly ``HEAD``). + :param subdomain: specifies the rule for the subdoain in case + subdomain matching is in use. + :param strict_slashes: can be used to disable the strict slashes + setting for this rule. See above. + :param options: other options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. + """ + def decorator(f): + self.add_url_rule(rule, None, f, **options) + return f + return decorator + + def errorhandler(self, code): + """A decorator that is used to register a function give a given + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register a function as error handler without using + the :meth:`errorhandler` decorator. The following example is + equivalent to the one above:: + + def page_not_found(error): + return 'This page does not exist', 404 + app.error_handlers[404] = page_not_found + + :param code: the code as integer for the handler + """ + def decorator(f): + self.error_handlers[code] = f + return f + return decorator + + def template_filter(self, name=None): + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + def decorator(f): + self.jinja_env.filters[name or f.__name__] = f + return f + return decorator + + def before_request(self, f): + """Registers a function to run before each request.""" + self.before_request_funcs.setdefault(None, []).append(f) + return f + + def after_request(self, f): + """Register a function to be run after each request.""" + self.after_request_funcs.setdefault(None, []).append(f) + return f + + def context_processor(self, f): + """Registers a template context processor function.""" + self.template_context_processors[None].append(f) + return f + + def handle_http_exception(self, e): + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionadded: 0.3 + """ + handler = self.error_handlers.get(e.code) + if handler is None: + return e + return handler(e) + + def handle_exception(self, e): + """Default exception handling that kicks in when an exception + occours that is not catched. In debug mode the exception will + be re-raised immediately, otherwise it is logged and the handler + for a 500 internal server error is used. If no such handler + exists, a default 500 internal server error message is displayed. + + .. versionadded: 0.3 + """ + handler = self.error_handlers.get(500) + if self.debug: + raise + self.logger.exception('Exception on %s [%s]' % ( + request.path, + request.method + )) + if handler is None: + return InternalServerError() + return handler(e) + + def dispatch_request(self): + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + """ + req = _request_ctx_stack.top.request + try: + if req.routing_exception is not None: + raise req.routing_exception + return self.view_functions[req.endpoint](**req.view_args) + except HTTPException, e: + return self.handle_http_exception(e) + + def make_response(self, rv): + """Converts the return value from a view function to a real + response object that is an instance of :attr:`response_class`. + + The following types are allowed for `rv`: + + .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| + + ======================= =========================================== + :attr:`response_class` the object is returned unchanged + :class:`str` a response object is created with the + string as body + :class:`unicode` a response object is created with the + string encoded to utf-8 as body + :class:`tuple` the response object is created with the + contents of the tuple as arguments + a WSGI function the function is called as WSGI application + and buffered as response object + ======================= =========================================== + + :param rv: the return value from the view function + """ + if rv is None: + raise ValueError('View function did not return a response') + if isinstance(rv, self.response_class): + return rv + if isinstance(rv, basestring): + return self.response_class(rv) + if isinstance(rv, tuple): + return self.response_class(*rv) + return self.response_class.force_type(rv, request.environ) + + def preprocess_request(self): + """Called before the actual request dispatching and will + call every as :meth:`before_request` decorated function. + If any of these function returns a value it's handled as + if it was the return value from the view and further + request handling is stopped. + """ + funcs = self.before_request_funcs.get(None, ()) + mod = request.module + if mod and mod in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[mod]) + for func in funcs: + rv = func() + if rv is not None: + return rv + + def process_response(self, response): + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = _request_ctx_stack.top + mod = ctx.request.module + if not isinstance(ctx.session, _NullSession): + self.save_session(ctx.session, response) + funcs = () + if mod and mod in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[mod]) + if None in self.after_request_funcs: + funcs = chain(funcs, self.after_request_funcs[None]) + for handler in funcs: + response = handler(response) + return response + + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.4 + The :meth:`after_request` functions are now called even if an + error handler took over request processing. This ensures that + even if an exception happens database have the chance to + properly close the connection. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + try: + response = self.process_response(response) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + + def request_context(self, environ): + """Creates a request context from the given environment and binds + it to the current context. This must be used in combination with + the `with` statement because the request is only bound to the + current context for the duration of the `with` block. + + Example usage:: + + with app.request_context(environ): + do_something_with(request) + + The object returned can also be used without the `with` statement + which is useful for working in the shell. The example above is + doing exactly the same as this code:: + + ctx = app.request_context(environ) + ctx.push() + try: + do_something_with(request) + finally: + ctx.pop() + + The big advantage of this approach is that you can use it without + the try/finally statement in a shell for interactive testing: + + >>> ctx = app.test_request_context() + >>> ctx.bind() + >>> request.path + u'/' + >>> ctx.unbind() + + .. versionchanged:: 0.3 + Added support for non-with statement usage and `with` statement + is now passed the ctx object. + + :param environ: a WSGI environment + """ + return _RequestContext(self, environ) + + def test_request_context(self, *args, **kwargs): + """Creates a WSGI environment from the given values (see + :func:`werkzeug.create_environ` for more information, this + function accepts the same arguments). + """ + return self.request_context(create_environ(*args, **kwargs)) + + def __call__(self, environ, start_response): + """Shortcut for :attr:`wsgi_app`.""" + return self.wsgi_app(environ, start_response) + diff --git a/flask/conf.py b/flask/conf.py new file mode 100644 index 00000000..e757212c --- /dev/null +++ b/flask/conf.py @@ -0,0 +1,135 @@ + + +class ConfigAttribute(object): + """Makes an attribute forward to the config""" + + def __init__(self, name): + self.__name__ = name + + def __get__(self, obj, type=None): + if obj is None: + return self + return obj.config[self.__name__] + + def __set__(self, obj, value): + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path, defaults=None): + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name, silent=False): + """Loads a configuration from an environment variable pointing to + a configuration file. This basically is just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to `True` if you want silent failing for missing + files. + :return: bool. `True` if able to load config, `False` otherwise. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError('The environment variable %r is not set ' + 'and as such configuration could not be ' + 'loaded. Set this variable and make it ' + 'point to a configuration file' % + variable_name) + self.from_pyfile(rv) + return True + + def from_pyfile(self, filename): + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + """ + filename = os.path.join(self.root_path, filename) + d = type(sys)('config') + d.__file__ = filename + execfile(filename, d.__dict__) + self.from_object(d) + + def from_object(self, obj): + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. + + Just the uppercase variables in that object are stored in the config + after lowercasing. Example usage:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + :param obj: an import name or object + """ + if isinstance(obj, basestring): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) diff --git a/flask/ctx.py b/flask/ctx.py new file mode 100644 index 00000000..88f61394 --- /dev/null +++ b/flask/ctx.py @@ -0,0 +1,57 @@ + + +class _RequestContext(object): + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + """ + + def __init__(self, app, environ): + self.app = app + self.url_adapter = app.url_map.bind_to_environ(environ, + server_name=app.config['SERVER_NAME']) + self.request = app.request_class(environ) + self.session = app.open_session(self.request) + if self.session is None: + self.session = _NullSession() + self.g = _RequestGlobals() + self.flashes = None + + try: + self.request.endpoint, self.request.view_args = \ + self.url_adapter.match() + except HTTPException, e: + self.request.routing_exception = e + + def push(self): + """Binds the request context.""" + _request_ctx_stack.push(self) + + def pop(self): + """Pops the request context.""" + _request_ctx_stack.pop() + + def __enter__(self): + self.push() + return self + + def __exit__(self, exc_type, exc_value, tb): + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + if not self.request.environ.get('flask._preserve_context') and \ + (tb is None or not self.app.debug): + self.pop() + +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) diff --git a/flask/helpers.py b/flask/helpers.py new file mode 100644 index 00000000..882cc544 --- /dev/null +++ b/flask/helpers.py @@ -0,0 +1,310 @@ +# try to load the best simplejson implementation available. If JSON +# is not installed, we add a failing class. +json_available = True +try: + import simplejson as json +except ImportError: + try: + import json + except ImportError: + json_available = False + +def _assert_have_json(): + """Helper function that fails if JSON is unavailable.""" + if not json_available: + raise RuntimeError('simplejson not installed') + +# figure out if simplejson escapes slashes. This behaviour was changed +# from one version to another without reason. +if not json_available or '\\/' not in json.dumps('/'): + + def _tojson_filter(*args, **kwargs): + if __debug__: + _assert_have_json() + return json.dumps(*args, **kwargs).replace('/', '\\/') +else: + _tojson_filter = json.dumps + +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. + + .. versionadded:: 0.2 + """ + if __debug__: + _assert_have_json() + return current_app.response_class(json.dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), mimetype='application/json') + + + +def url_for(endpoint, **values): + """Generates a URL to the given endpoint with the method provided. + The endpoint is relative to the active module if modules are in use. + + Here some examples: + + ==================== ======================= ============================= + Active Module Target Endpoint Target Function + ==================== ======================= ============================= + `None` ``'index'`` `index` of the application + `None` ``'.index'`` `index` of the application + ``'admin'`` ``'index'`` `index` of the `admin` module + any ``'.index'`` `index` of the application + any ``'admin.index'`` `index` of the `admin` module + ==================== ======================= ============================= + + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. + + For more information, head over to the :ref:`Quickstart `. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :param _external: if set to `True`, an absolute URL is generated. + """ + ctx = _request_ctx_stack.top + if '.' not in endpoint: + mod = ctx.request.module + if mod is not None: + endpoint = mod + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] + external = values.pop('_external', False) + return ctx.url_adapter.build(endpoint, values, force_external=external) + + +def get_template_attribute(template_name, attribute): + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named `_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to acccess + """ + return getattr(current_app.jinja_env.get_template(template_name).module, + attribute) + + +def flash(message, category='message'): + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + session.setdefault('_flashes', []).append((category, message)) + + +def get_flashed_messages(with_categories=False): + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to `True`, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Example usage: + + .. sourcecode:: html+jinja + + {% for category, msg in get_flashed_messages(with_categories=true) %} +

    {{ msg }} + {% endfor %} + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + :param with_categories: set to `True` to also receive categories. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + + +def send_file(filename_or_fp, mimetype=None, as_attachment=False, + attachment_filename=None): + """Sends the contents of a file to the client. This will use the + most efficient method available and configured. By default it will + try to use the WSGI server's file_wrapper support. Alternatively + you can set the application's :attr:`~Flask.use_x_sendfile` attribute + to ``True`` to directly emit an `X-Sendfile` header. This however + requires support of the underlying webserver for `X-Sendfile`. + + By default it will try to guess the mimetype for you, but you can + also explicitly provide one. For extra security you probably want + to sent certain files as attachment (HTML for instance). + + Please never pass filenames to this function from user sources without + checking them first. Something like this is usually sufficient to + avoid security problems:: + + if '..' in filename or filename.startswith('/'): + abort(404) + + .. versionadded:: 0.2 + + :param filename_or_fp: the filename of the file to send. This is + relative to the :attr:`~Flask.root_path` if a + relative path is specified. + Alternatively a file object might be provided + in which case `X-Sendfile` might not work and + fall back to the traditional method. + :param mimetype: the mimetype of the file if provided, otherwise + auto detection happens. + :param as_attachment: set to `True` if you want to send this file with + a ``Content-Disposition: attachment`` header. + :param attachment_filename: the filename for the attachment if it + differs from the file's filename. + """ + if isinstance(filename_or_fp, basestring): + filename = filename_or_fp + file = None + else: + file = filename_or_fp + filename = getattr(file, 'name', None) + if filename is not None: + filename = os.path.join(current_app.root_path, filename) + if mimetype is None and (filename or attachment_filename): + mimetype = mimetypes.guess_type(filename or attachment_filename)[0] + if mimetype is None: + mimetype = 'application/octet-stream' + + headers = Headers() + if as_attachment: + if attachment_filename is None: + if filename is None: + raise TypeError('filename unavailable, required for ' + 'sending as attachment') + attachment_filename = os.path.basename(filename) + headers.add('Content-Disposition', 'attachment', + filename=attachment_filename) + + if current_app.use_x_sendfile and filename: + if file is not None: + file.close() + headers['X-Sendfile'] = filename + data = None + else: + if file is None: + file = open(filename, 'rb') + data = wrap_file(request.environ, file) + + return Response(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + current_app.update_template_context(context) + return current_app.jinja_env.get_template(template_name).render(context) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + current_app.update_template_context(context) + return current_app.jinja_env.from_string(source).render(context) + + + +def _get_package_path(name): + """Returns the path to a package or cwd if that cannot be found.""" + try: + return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) + except (KeyError, AttributeError): + return os.getcwd() + + + +class _PackageBoundObject(object): + + def __init__(self, import_name): + #: The name of the package or module. Do not change this once + #: it was set by the constructor. + self.import_name = import_name + + #: Where is the app root located? + self.root_path = _get_package_path(self.import_name) + + def open_resource(self, resource): + """Opens a resource from the application's resource folder. To see + how this works, consider the following folder structure:: + + /myapplication.py + /schemal.sql + /static + /style.css + /templates + /layout.html + /index.html + + If you want to open the `schema.sql` file you would do the + following:: + + with app.open_resource('schema.sql') as f: + contents = f.read() + do_something_with(contents) + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + if pkg_resources is None: + return open(os.path.join(self.root_path, resource), 'rb') + return pkg_resources.resource_stream(self.import_name, resource) + diff --git a/flask/module.py b/flask/module.py new file mode 100644 index 00000000..2d53f3a9 --- /dev/null +++ b/flask/module.py @@ -0,0 +1,151 @@ + + +class _ModuleSetupState(object): + + def __init__(self, app, url_prefix=None): + self.app = app + self.url_prefix = url_prefix + + +class Module(_PackageBoundObject): + """Container object that enables pluggable applications. A module can + be used to organize larger applications. They represent blueprints that, + in combination with a :class:`Flask` object are used to create a large + application. + + A module is like an application bound to an `import_name`. Multiple + modules can share the same import names, but in that case a `name` has + to be provided to keep them apart. If different import names are used, + the rightmost part of the import name is used as name. + + Here an example structure for a larger appliation:: + + /myapplication + /__init__.py + /views + /__init__.py + /admin.py + /frontend.py + + The `myapplication/__init__.py` can look like this:: + + from flask import Flask + from myapplication.views.admin import admin + from myapplication.views.frontend import frontend + + app = Flask(__name__) + app.register_module(admin, url_prefix='/admin') + app.register_module(frontend) + + And here an example view module (`myapplication/views/admin.py`):: + + from flask import Module + + admin = Module(__name__) + + @admin.route('/') + def index(): + pass + + @admin.route('/login') + def login(): + pass + + For a gentle introduction into modules, checkout the + :ref:`working-with-modules` section. + """ + + def __init__(self, import_name, name=None, url_prefix=None): + if name is None: + assert '.' in import_name, 'name required if package name ' \ + 'does not point to a submodule' + name = import_name.rsplit('.', 1)[1] + _PackageBoundObject.__init__(self, import_name) + self.name = name + self.url_prefix = url_prefix + self._register_events = [] + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a module. The endpoint for the + :func:`url_for` function is prefixed with the name of the module. + """ + def decorator(f): + self.add_url_rule(rule, f.__name__, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for + the :func:`url_for` function is prefixed with the name of the module. + """ + def register_rule(state): + the_rule = rule + if state.url_prefix: + the_rule = state.url_prefix + rule + state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), + view_func, **options) + self._record(register_rule) + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a module. This function + is only executed before each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a module. This function + is only executed after each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a module. Such a function + is executed after each request, even if outside of the module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. This + function is only executed for requests handled by a module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. Such a + function is executed each request, even if outside of the module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a module. This + handler is used for all requests, even if outside of the module. + + .. versionadded:: 0.4 + """ + def decorator(f): + self._record(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator + + def _record(self, func): + self._register_events.append(func) diff --git a/flask/session.py b/flask/session.py new file mode 100644 index 00000000..11951962 --- /dev/null +++ b/flask/session.py @@ -0,0 +1,28 @@ +class Session(SecureCookie): + """Expands the session with support for switching between permanent + and non-permanent sessions. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + + def _set_permanent(self, value): + self['_permanent'] = bool(value) + + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + +class _NullSession(Session): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail diff --git a/flask/wrappers.py b/flask/wrappers.py new file mode 100644 index 00000000..933b28a8 --- /dev/null +++ b/flask/wrappers.py @@ -0,0 +1,57 @@ +class Request(RequestBase): + """The request object used by default in flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + """ + + #: the endpoint that matched the request. This in combination with + #: :attr:`view_args` can be used to reconstruct the same or a + #: modified URL. If an exception happened when matching, this will + #: be `None`. + endpoint = None + + #: a dict of view arguments that matched the request. If an exception + #: happened when matching, this will be `None`. + view_args = None + + #: if matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception = None + + @property + def module(self): + """The name of the current module""" + if self.endpoint and '.' in self.endpoint: + return self.endpoint.rsplit('.', 1)[0] + + @cached_property + def json(self): + """If the mimetype is `application/json` this will contain the + parsed JSON data. + """ + if __debug__: + _assert_have_json() + if self.mimetype == 'application/json': + return json.loads(self.data) + + +class Response(ResponseBase): + """The response object that is used by default in flask. Works like the + response object from Werkzeug but is set to have a HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + """ + default_mimetype = 'text/html' + + +class _RequestGlobals(object): + pass + From c4f64c1c475badaeef009ad46846e32a8e5c9b66 Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 15:10:32 -0400 Subject: [PATCH 0317/3747] working import layout for module --- flask/__init__.py | 47 ++++++----------------------------------------- flask/app.py | 24 ++++++++++++++++++++++-- flask/conf.py | 4 ++++ flask/ctx.py | 5 +++++ flask/globals.py | 8 ++++++++ flask/helpers.py | 26 ++++++++++++++++++++++---- flask/module.py | 1 + flask/session.py | 3 +++ flask/wrappers.py | 7 +++++++ 9 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 flask/globals.py diff --git a/flask/__init__.py b/flask/__init__.py index f7ae2551..e20f1206 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -9,50 +9,15 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement -import os -import sys -import mimetypes -from datetime import datetime, timedelta - -# this is a workaround for appengine. Do not remove this import -import werkzeug - -from itertools import chain -from threading import Lock -from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import Request as RequestBase, Response as ResponseBase, \ - LocalStack, LocalProxy, create_environ, SharedDataMiddleware, \ - ImmutableDict, cached_property, wrap_file, Headers, \ - import_string -from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError -from werkzeug.contrib.securecookie import SecureCookie - - # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. from werkzeug import abort, redirect from jinja2 import Markup, escape -# use pkg_resource if that works, otherwise fall back to cwd. The -# current working directory is generally not reliable with the notable -# exception of google appengine. -try: - import pkg_resources - pkg_resources.resource_stream -except (ImportError, AttributeError): - pkg_resources = None - -# a lock used for logger initialization -_logger_lock = Lock() - - - -# context locals -_request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) +from flask.app import Flask +from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ + get_flashed_messages, render_template, render_template, render_template_string, \ + get_template_attribute +from flask.globals import current_app, g, request, session, _request_ctx_stack +from flask.module import Module diff --git a/flask/app.py b/flask/app.py index f3e1f73c..654a96f4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1,3 +1,23 @@ +from threading import Lock +from datetime import timedelta, datetime +from itertools import chain + +from jinja2 import Environment, PackageLoader, FileSystemLoader +from werkzeug import ImmutableDict, SharedDataMiddleware, create_environ +from werkzeug.routing import Map, Rule +from werkzeug.exceptions import HTTPException, InternalServerError + +from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ + _tojson_filter, get_pkg_resources +from flask.wrappers import Request, Response +from flask.conf import ConfigAttribute, Config +from flask.ctx import _default_template_ctx_processor, _RequestContext +from flask.globals import _request_ctx_stack, request +from flask.session import Session, _NullSession +from flask.module import _ModuleSetupState + +# a lock used for logger initialization +_logger_lock = Lock() class Flask(_PackageBoundObject): @@ -219,7 +239,7 @@ class Flask(_PackageBoundObject): if self.static_path is not None: self.add_url_rule(self.static_path + '/', build_only=True, endpoint='static') - if pkg_resources is not None: + if get_pkg_resources() is not None: target = (self.import_name, 'static') else: target = os.path.join(self.root_path, 'static') @@ -279,7 +299,7 @@ class Flask(_PackageBoundObject): `templates` folder. To add other loaders it's possible to override this method. """ - if pkg_resources is None: + if get_pkg_resources() is None: return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) diff --git a/flask/conf.py b/flask/conf.py index e757212c..90414155 100644 --- a/flask/conf.py +++ b/flask/conf.py @@ -1,3 +1,7 @@ +import os +import sys + +from werkzeug import import_string class ConfigAttribute(object): diff --git a/flask/ctx.py b/flask/ctx.py index 88f61394..f3eab16a 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -1,3 +1,8 @@ +from werkzeug.exceptions import HTTPException + +from flask.wrappers import _RequestGlobals +from flask.globals import _request_ctx_stack +from flask.session import _NullSession class _RequestContext(object): diff --git a/flask/globals.py b/flask/globals.py new file mode 100644 index 00000000..11fdf0b3 --- /dev/null +++ b/flask/globals.py @@ -0,0 +1,8 @@ +from werkzeug import LocalStack, LocalProxy + +# context locals +_request_ctx_stack = LocalStack() +current_app = LocalProxy(lambda: _request_ctx_stack.top.app) +request = LocalProxy(lambda: _request_ctx_stack.top.request) +session = LocalProxy(lambda: _request_ctx_stack.top.session) +g = LocalProxy(lambda: _request_ctx_stack.top.g) \ No newline at end of file diff --git a/flask/helpers.py b/flask/helpers.py index 882cc544..58dd2094 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -1,3 +1,7 @@ +import os +import sys +import mimetypes + # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. json_available = True @@ -9,6 +13,12 @@ except ImportError: except ImportError: json_available = False +from werkzeug import Headers, wrap_file + +from flask.globals import session, _request_ctx_stack, current_app, request +from flask.wrappers import Response + + def _assert_have_json(): """Helper function that fails if JSON is unavailable.""" if not json_available: @@ -57,6 +67,17 @@ def jsonify(*args, **kwargs): return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') +def get_pkg_resources(): + """Use pkg_resource if that works, otherwise fall back to cwd. The + current working directory is generally not reliable with the notable + exception of google appengine. + """ + try: + import pkg_resources + pkg_resources.resource_stream + except (ImportError, AttributeError): + return + return pkg_resources def url_for(endpoint, **values): @@ -164,7 +185,6 @@ def get_flashed_messages(with_categories=False): return flashes - def send_file(filename_or_fp, mimetype=None, as_attachment=False, attachment_filename=None): """Sends the contents of a file to the client. This will use the @@ -262,7 +282,6 @@ def render_template_string(source, **context): return current_app.jinja_env.from_string(source).render(context) - def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: @@ -271,7 +290,6 @@ def _get_package_path(name): return os.getcwd() - class _PackageBoundObject(object): def __init__(self, import_name): @@ -304,7 +322,7 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ + pkg_resources = get_pkg_resources() if pkg_resources is None: return open(os.path.join(self.root_path, resource), 'rb') return pkg_resources.resource_stream(self.import_name, resource) - diff --git a/flask/module.py b/flask/module.py index 2d53f3a9..3d691316 100644 --- a/flask/module.py +++ b/flask/module.py @@ -1,3 +1,4 @@ +from flask.helpers import _PackageBoundObject class _ModuleSetupState(object): diff --git a/flask/session.py b/flask/session.py index 11951962..cd0d5d29 100644 --- a/flask/session.py +++ b/flask/session.py @@ -1,3 +1,6 @@ +from werkzeug.contrib.securecookie import SecureCookie + + class Session(SecureCookie): """Expands the session with support for switching between permanent and non-permanent sessions. diff --git a/flask/wrappers.py b/flask/wrappers.py index 933b28a8..091c708e 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -1,3 +1,9 @@ +from werkzeug import Request as RequestBase, Response as ResponseBase, \ + cached_property + +from helpers import json + + class Request(RequestBase): """The request object used by default in flask. Remembers the matched endpoint and view arguments. @@ -35,6 +41,7 @@ class Request(RequestBase): parsed JSON data. """ if __debug__: + from flask.helpers import _assert_have_json _assert_have_json() if self.mimetype == 'application/json': return json.loads(self.data) From 1a69c7d4bf4d87d00b5fb2551ea39ee7c932768b Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Fri, 2 Jul 2010 15:11:02 -0400 Subject: [PATCH 0318/3747] look for json module in the right place. all tests now pass with the new module layout --- tests/flask_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1c5dc729..582f2e05 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,7 +21,7 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from cStringIO import StringIO - +from flask.helpers import json example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') sys.path.append(os.path.join(example_path, 'flaskr')) @@ -409,7 +409,7 @@ class JSONTestCase(unittest.TestCase): for url in '/kw', '/dict': rv = c.get(url) assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d + assert json.loads(rv.data) == d def test_json_attr(self): app = flask.Flask(__name__) @@ -417,7 +417,7 @@ class JSONTestCase(unittest.TestCase): def add(): return unicode(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + rv = c.post('/add', data=json.dumps({'a': 1, 'b': 2}), content_type='application/json') assert rv.data == '3' From b2c7efd0fabcc0514e27d03ed30f5f8af1de1065 Mon Sep 17 00:00:00 2001 From: Marcus Fredriksson Date: Fri, 2 Jul 2010 00:26:41 +0800 Subject: [PATCH 0319/3747] Typo --- flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask.py b/flask.py index 9c720ef0..c2ccd6a9 100644 --- a/flask.py +++ b/flask.py @@ -1292,7 +1292,7 @@ class Flask(_PackageBoundObject): :param methods: a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case + :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. From 7599046d042d1bb3f2db6cdcd6458338802901c4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:16:24 +0200 Subject: [PATCH 0320/3747] Started working on refactoring Jinja integration --- CHANGES | 4 ++++ docs/quickstart.rst | 7 +++++++ flask.py | 42 ++++++++++++++++++++++++++++++++-------- tests/flask_tests.py | 8 ++++++++ tests/templates/mail.txt | 1 + 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/templates/mail.txt diff --git a/CHANGES b/CHANGES index 3490f0e2..7336c377 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,10 @@ Codename to be decided, release date to be announced. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with the `SERVER_NAME` config key. +- autoescaping is no longer active for all templates. Instead it + is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. + Inside templates this behaviour can be changed with the + ``autoescape`` tag. Version 0.4 ----------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a4858f1c..e1fdce51 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -414,6 +414,13 @@ Markup(u'<blink>hacker</blink>') >>> Markup('Marked up » HTML').striptags() u'Marked up \xbb HTML' +.. versionchanged:: 0.5 + + Autoescaping is no longer enabled for all templates. The following + extensions for templates trigger autoescaping: ``.html``, ``.htm``, + ``.xml``, ``.xhtml``. Templates loaded from string will have + autoescaping disabled. + .. [#] Unsure what that :class:`~flask.g` object is? It's something you can store information on yourself, check the documentation of that object (:class:`~flask.g`) and the :ref:`sqlite3` for more diff --git a/flask.py b/flask.py index 9c720ef0..c55838dc 100644 --- a/flask.py +++ b/flask.py @@ -788,6 +788,15 @@ class Config(dict): return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) +def _select_autoescape(filename): + """Returns `True` if autoescaping should be active for the given + template name. + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -920,7 +929,7 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=True, + autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -1018,13 +1027,8 @@ class Flask(_PackageBoundObject): #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter + self.jinja_env = self.create_jinja_environment() + self.init_jinja_globals() @property def logger(self): @@ -1061,6 +1065,15 @@ class Flask(_PackageBoundObject): self._logger = logger return logger + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`create_jinja_loader`. + + .. versionadded:: 0.5 + """ + return Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for the configured package is returned that looks up templates in the @@ -1071,6 +1084,19 @@ class Flask(_PackageBoundObject): return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) + def init_jinja_globals(self): + """Callde directly after the environment was created to inject + some defaults (like `url_for`, `get_flashed_messages` and the + `tojson` filter. + + .. versionadded:: 0.5 + """ + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1c5dc729..4309f214 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -461,6 +461,14 @@ class TemplatingTestCase(unittest.TestCase): '

    Hello World!' ] + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + assert flask.render_template_string('{{ foo }}', + foo='') == '' + assert flask.render_template('mail.txt', foo='') \ + == ' Mail' + def test_macros(self): app = flask.Flask(__name__) with app.test_request_context(): diff --git a/tests/templates/mail.txt b/tests/templates/mail.txt new file mode 100644 index 00000000..d6cb92ea --- /dev/null +++ b/tests/templates/mail.txt @@ -0,0 +1 @@ +{{ foo}} Mail From 8798b4b7112fb984bd244c8581ab03ef4a7ba766 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:25:26 +0200 Subject: [PATCH 0321/3747] Merged in changes from master by hand --- flask/__init__.py | 2 +- flask/app.py | 42 ++++++++++++++++++++++++++++++++++-------- tests/flask_tests.py | 5 ++--- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index e20f1206..b57e7d07 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,6 +18,6 @@ from jinja2 import Markup, escape from flask.app import Flask from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ - get_template_attribute + get_template_attribute, json from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module diff --git a/flask/app.py b/flask/app.py index 654a96f4..9fd7512b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -20,6 +20,15 @@ from flask.module import _ModuleSetupState _logger_lock = Lock() +def _select_autoescape(filename): + """Returns `True` if autoescaping should be active for the given + template name. + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -152,7 +161,7 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=True, + autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -250,13 +259,8 @@ class Flask(_PackageBoundObject): #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned #: by the :meth:`create_jinja_loader` function. - self.jinja_env = Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter + self.jinja_env = self.create_jinja_environment() + self.init_jinja_globals() @property def logger(self): @@ -293,6 +297,15 @@ class Flask(_PackageBoundObject): self._logger = logger return logger + def create_jinja_environment(self): + """Creates the Jinja2 environment based on :attr:`jinja_options` + and :meth:`create_jinja_loader`. + + .. versionadded:: 0.5 + """ + return Environment(loader=self.create_jinja_loader(), + **self.jinja_options) + def create_jinja_loader(self): """Creates the Jinja loader. By default just a package loader for the configured package is returned that looks up templates in the @@ -303,6 +316,19 @@ class Flask(_PackageBoundObject): return FileSystemLoader(os.path.join(self.root_path, 'templates')) return PackageLoader(self.import_name) + def init_jinja_globals(self): + """Callde directly after the environment was created to inject + some defaults (like `url_for`, `get_flashed_messages` and the + `tojson` filter. + + .. versionadded:: 0.5 + """ + self.jinja_env.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + self.jinja_env.filters['tojson'] = _tojson_filter + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 08194f6e..7e2c9246 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,7 +21,6 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from cStringIO import StringIO -from flask.helpers import json example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') sys.path.append(os.path.join(example_path, 'flaskr')) @@ -409,7 +408,7 @@ class JSONTestCase(unittest.TestCase): for url in '/kw', '/dict': rv = c.get(url) assert rv.mimetype == 'application/json' - assert json.loads(rv.data) == d + assert flask.json.loads(rv.data) == d def test_json_attr(self): app = flask.Flask(__name__) @@ -417,7 +416,7 @@ class JSONTestCase(unittest.TestCase): def add(): return unicode(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() - rv = c.post('/add', data=json.dumps({'a': 1, 'b': 2}), + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') assert rv.data == '3' From dd59d7241d0ebc713d51ab939f53ebd0df8b2dac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:26:34 +0200 Subject: [PATCH 0322/3747] Ported a typo fix from master --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 9fd7512b..273e9b34 100644 --- a/flask/app.py +++ b/flask/app.py @@ -550,7 +550,7 @@ class Flask(_PackageBoundObject): :param methods: a list of methods this rule should be limited to (``GET``, ``POST`` etc.). By default a rule just listens for ``GET`` (and implicitly ``HEAD``). - :param subdomain: specifies the rule for the subdoain in case + :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. From 4f8ee8f12946b224e5003405be99aa4e454bdeee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:35:20 +0200 Subject: [PATCH 0323/3747] Added missing comments, fixed setup.py and made tests pass --- flask/__init__.py | 1 + flask/app.py | 13 ++++++++++++- flask/{conf.py => config.py} | 11 +++++++++++ flask/ctx.py | 16 +++++++++++++++- flask/globals.py | 14 +++++++++++++- flask/helpers.py | 19 ++++++++++++++++--- flask/module.py | 11 +++++++++++ flask/session.py | 12 ++++++++++++ flask/wrappers.py | 16 +++++++++++----- setup.py | 2 +- 10 files changed, 103 insertions(+), 12 deletions(-) rename flask/{conf.py => config.py} (96%) diff --git a/flask/__init__.py b/flask/__init__.py index b57e7d07..8cfe7fca 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -16,6 +16,7 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape from flask.app import Flask +from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ get_template_attribute, json diff --git a/flask/app.py b/flask/app.py index 273e9b34..e7ed5bcb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.app + ~~~~~~~~~ + + This module implements the central WSGI application object. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -10,7 +21,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, get_pkg_resources from flask.wrappers import Request, Response -from flask.conf import ConfigAttribute, Config +from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession diff --git a/flask/conf.py b/flask/config.py similarity index 96% rename from flask/conf.py rename to flask/config.py index 90414155..7918de01 100644 --- a/flask/conf.py +++ b/flask/config.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.config + ~~~~~~~~~~~~ + + Implements the configuration related objects. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + import os import sys diff --git a/flask/ctx.py b/flask/ctx.py index f3eab16a..1c538ecf 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -1,10 +1,24 @@ +# -*- coding: utf-8 -*- +""" + flask.ctx + ~~~~~~~~~ + + Implements the objects required to keep the context. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug.exceptions import HTTPException -from flask.wrappers import _RequestGlobals from flask.globals import _request_ctx_stack from flask.session import _NullSession +class _RequestGlobals(object): + pass + + class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the diff --git a/flask/globals.py b/flask/globals.py index 11fdf0b3..aac46555 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -1,3 +1,15 @@ +# -*- coding: utf-8 -*- +""" + flask.globals + ~~~~~~~~~~~~~ + + Defines all the global objects that are proxies to the current + active context. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug import LocalStack, LocalProxy # context locals @@ -5,4 +17,4 @@ _request_ctx_stack = LocalStack() current_app = LocalProxy(lambda: _request_ctx_stack.top.app) request = LocalProxy(lambda: _request_ctx_stack.top.request) session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) \ No newline at end of file +g = LocalProxy(lambda: _request_ctx_stack.top.g) diff --git a/flask/helpers.py b/flask/helpers.py index 58dd2094..5a6e3966 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.helpers + ~~~~~~~~~~~~~ + + Implements various helpers. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + import os import sys import mimetypes @@ -12,13 +23,13 @@ except ImportError: import json except ImportError: json_available = False - + from werkzeug import Headers, wrap_file from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response - + def _assert_have_json(): """Helper function that fails if JSON is unavailable.""" if not json_available: @@ -35,6 +46,7 @@ if not json_available or '\\/' not in json.dumps('/'): else: _tojson_filter = json.dumps + def jsonify(*args, **kwargs): """Creates a :class:`~flask.Response` with the JSON representation of the given arguments with an `application/json` mimetype. The arguments @@ -66,7 +78,8 @@ def jsonify(*args, **kwargs): _assert_have_json() return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') - + + def get_pkg_resources(): """Use pkg_resource if that works, otherwise fall back to cwd. The current working directory is generally not reliable with the notable diff --git a/flask/module.py b/flask/module.py index 3d691316..85898511 100644 --- a/flask/module.py +++ b/flask/module.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.module + ~~~~~~~~~~~~ + + Implements a class that represents module blueprints. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from flask.helpers import _PackageBoundObject diff --git a/flask/session.py b/flask/session.py index cd0d5d29..324fc98d 100644 --- a/flask/session.py +++ b/flask/session.py @@ -1,3 +1,15 @@ +# -*- coding: utf-8 -*- +""" + flask.session + ~~~~~~~~~~~~~ + + Implements cookie based sessions based on Werkzeug's secure cookie + system. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug.contrib.securecookie import SecureCookie diff --git a/flask/wrappers.py b/flask/wrappers.py index 091c708e..d962a563 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + flask.wrappers + ~~~~~~~~~~~~~~ + + Implements the WSGI wrappers (request and response). + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property @@ -57,8 +68,3 @@ class Response(ResponseBase): set :attr:`~flask.Flask.response_class` to your subclass. """ default_mimetype = 'text/html' - - -class _RequestGlobals(object): - pass - diff --git a/setup.py b/setup.py index 404fad32..dd3561da 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - py_modules=['flask'], + packages=['flask'], zip_safe=False, platforms='any', install_requires=[ From 88d9315d1955ea4ee99f53e21593a0d1d9047ed0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:37:39 +0200 Subject: [PATCH 0324/3747] We should now have the same public API as before --- CHANGES | 2 ++ flask/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7336c377..795c2eb8 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Codename to be decided, release date to be announced. is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside templates this behaviour can be changed with the ``autoescape`` tag. +- refactored Flask internally. It now consists of more than a + single file. Version 0.4 ----------- diff --git a/flask/__init__.py b/flask/__init__.py index 8cfe7fca..85c8dd3f 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -15,7 +15,7 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape -from flask.app import Flask +from flask.app import Flask, Request, Response from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ get_flashed_messages, render_template, render_template, render_template_string, \ From d0c6ad7d287e543fcc941aa2b42557e06b9dc142 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 11:56:08 +0200 Subject: [PATCH 0325/3747] Added support for conditional responses to send_file --- CHANGES | 2 ++ flask/app.py | 2 +- flask/helpers.py | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 795c2eb8..1440781e 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,8 @@ Codename to be decided, release date to be announced. ``autoescape`` tag. - refactored Flask internally. It now consists of more than a single file. +- :func:`flask.send_file` now emits etags and has the ability to + do conditional responses builtin. Version 0.4 ----------- diff --git a/flask/app.py b/flask/app.py index e7ed5bcb..062d4289 100644 --- a/flask/app.py +++ b/flask/app.py @@ -328,7 +328,7 @@ class Flask(_PackageBoundObject): return PackageLoader(self.import_name) def init_jinja_globals(self): - """Callde directly after the environment was created to inject + """Called directly after the environment was created to inject some defaults (like `url_for`, `get_flashed_messages` and the `tojson` filter. diff --git a/flask/helpers.py b/flask/helpers.py index 5a6e3966..a34e055e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -12,6 +12,8 @@ import os import sys import mimetypes +from time import time +from zlib import adler32 # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -24,7 +26,7 @@ except ImportError: except ImportError: json_available = False -from werkzeug import Headers, wrap_file +from werkzeug import Headers, wrap_file, is_resource_modified from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response @@ -199,7 +201,8 @@ def get_flashed_messages(with_categories=False): def send_file(filename_or_fp, mimetype=None, as_attachment=False, - attachment_filename=None): + attachment_filename=None, add_etags=True, + cache_timeout=60 * 60 * 12, conditional=False): """Sends the contents of a file to the client. This will use the most efficient method available and configured. By default it will try to use the WSGI server's file_wrapper support. Alternatively @@ -220,6 +223,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionadded:: 0.2 + .. versionadded:: 0.5 + The `add_etags`, `cache_timeout` and `conditional` parameters were added. + The default behaviour is now to attach etags. + :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. @@ -232,6 +239,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, a ``Content-Disposition: attachment`` header. :param attachment_filename: the filename for the attachment if it differs from the file's filename. + :param add_etags: set to `False` to disable attaching of etags. + :param conditional: set to `True` to enable conditional responses. + :param cache_timeout: the timeout in seconds for the headers. """ if isinstance(filename_or_fp, basestring): filename = filename_or_fp @@ -266,8 +276,27 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = open(filename, 'rb') data = wrap_file(request.environ, file) - return Response(data, mimetype=mimetype, headers=headers, - direct_passthrough=True) + rv = Response(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) + + rv.cache_control.public = True + if cache_timeout: + rv.cache_control.max_age = cache_timeout + rv.expires = int(time() + cache_timeout) + + if add_etags and filename is not None: + rv.set_etag('flask-%s-%s-%s' % ( + os.path.getmtime(filename), + os.path.getsize(filename), + adler32(filename) & 0xffffffff + )) + if conditional: + rv = rv.make_conditional(request) + # make sure we don't send x-sendfile for servers that + # ignore the 304 status code for x-sendfile. + if rv.status_code == 304: + rv.headers.pop('x-sendfile', None) + return rv def render_template(template_name, **context): From 532347d6adf1da64259f1af91860d5fa27bac9f1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:06:29 +0200 Subject: [PATCH 0326/3747] phased out shared data middleware in flask for send_file --- flask/app.py | 32 +++++++++++++++++++++----------- flask/helpers.py | 3 ++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/flask/app.py b/flask/app.py index 062d4289..7230950c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -9,17 +9,19 @@ :license: BSD, see LICENSE for more details. """ +import os +import posixpath from threading import Lock from datetime import timedelta, datetime from itertools import chain from jinja2 import Environment, PackageLoader, FileSystemLoader -from werkzeug import ImmutableDict, SharedDataMiddleware, create_environ +from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, get_pkg_resources + _tojson_filter, get_pkg_resources, send_file from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -258,14 +260,8 @@ class Flask(_PackageBoundObject): if self.static_path is not None: self.add_url_rule(self.static_path + '/', - build_only=True, endpoint='static') - if get_pkg_resources() is not None: - target = (self.import_name, 'static') - else: - target = os.path.join(self.root_path, 'static') - self.wsgi_app = SharedDataMiddleware(self.wsgi_app, { - self.static_path: target - }) + endpoint='static', + view_func=self.send_static_file) #: The Jinja2 environment. It is created from the #: :attr:`jinja_options` and the loader that is returned @@ -383,6 +379,20 @@ class Flask(_PackageBoundObject): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + filename = posixpath.normpath(filename) + if filename.startswith('../'): + raise NotFound() + filename = os.path.join(self.root_path, 'static', filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True) + def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. diff --git a/flask/helpers.py b/flask/helpers.py index a34e055e..ce206eea 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -250,7 +250,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = filename_or_fp filename = getattr(file, 'name', None) if filename is not None: - filename = os.path.join(current_app.root_path, filename) + if not os.path.isabs(filename): + filename = os.path.join(current_app.root_path, filename) if mimetype is None and (filename or attachment_filename): mimetype = mimetypes.guess_type(filename or attachment_filename)[0] if mimetype is None: From fedc06c2950a4c9930082a014d0f8da6ee0193be Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:14:00 +0200 Subject: [PATCH 0327/3747] dropped pkg_resources --- CHANGES | 2 ++ flask/app.py | 6 ++---- flask/helpers.py | 18 +----------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/CHANGES b/CHANGES index 1440781e..34cd23d8 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,8 @@ Codename to be decided, release date to be announced. single file. - :func:`flask.send_file` now emits etags and has the ability to do conditional responses builtin. +- (temporarily) dropped support for zipped applications. This was a + rarely used feature and led to some confusing behaviour. Version 0.4 ----------- diff --git a/flask/app.py b/flask/app.py index 7230950c..c902f276 100644 --- a/flask/app.py +++ b/flask/app.py @@ -21,7 +21,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, get_pkg_resources, send_file + _tojson_filter, send_file from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -319,9 +319,7 @@ class Flask(_PackageBoundObject): `templates` folder. To add other loaders it's possible to override this method. """ - if get_pkg_resources() is None: - return FileSystemLoader(os.path.join(self.root_path, 'templates')) - return PackageLoader(self.import_name) + return FileSystemLoader(os.path.join(self.root_path, 'templates')) def init_jinja_globals(self): """Called directly after the environment was created to inject diff --git a/flask/helpers.py b/flask/helpers.py index ce206eea..0afd4a85 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -82,19 +82,6 @@ def jsonify(*args, **kwargs): indent=None if request.is_xhr else 2), mimetype='application/json') -def get_pkg_resources(): - """Use pkg_resource if that works, otherwise fall back to cwd. The - current working directory is generally not reliable with the notable - exception of google appengine. - """ - try: - import pkg_resources - pkg_resources.resource_stream - except (ImportError, AttributeError): - return - return pkg_resources - - def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. @@ -365,7 +352,4 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ - pkg_resources = get_pkg_resources() - if pkg_resources is None: - return open(os.path.join(self.root_path, resource), 'rb') - return pkg_resources.resource_stream(self.import_name, resource) + return open(os.path.join(self.root_path, resource), 'rb') From 15012af70017962c2a22bc3fe670b9cc50782366 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 12:48:37 +0200 Subject: [PATCH 0328/3747] Refactored static folder registration --- flask/app.py | 27 +++++++++------------------ flask/helpers.py | 24 ++++++++++++++++++++++++ flask/module.py | 28 +++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/flask/app.py b/flask/app.py index c902f276..a818ae62 100644 --- a/flask/app.py +++ b/flask/app.py @@ -10,7 +10,6 @@ """ import os -import posixpath from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -21,7 +20,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, send_file + _tojson_filter from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config from flask.ctx import _default_template_ctx_processor, _RequestContext @@ -101,6 +100,9 @@ class Flask(_PackageBoundObject): #: Path for the static files. If you don't want to use static files #: you can set this value to `None` in which case no URL rule is added #: and the development server will no longer serve any static files. + #: + #: This is the default used for application and modules unless a + #: different value is passed to the constructor. static_path = '/static' #: The debug flag. Set this to `True` to enable debugging of the @@ -190,8 +192,10 @@ class Flask(_PackageBoundObject): 'SERVER_NAME': None }) - def __init__(self, import_name): + def __init__(self, import_name, static_path=None): _PackageBoundObject.__init__(self, import_name) + if static_path is not None: + self.static_path = static_path #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods @@ -258,7 +262,8 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() - if self.static_path is not None: + # if there is a static folder, register it for the application. + if self.has_static_folder: self.add_url_rule(self.static_path + '/', endpoint='static', view_func=self.send_static_file) @@ -377,20 +382,6 @@ class Flask(_PackageBoundObject): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) - def send_static_file(self, filename): - """Function used internally to send static files from the static - folder to the browser. - - .. versionadded:: 0.5 - """ - filename = posixpath.normpath(filename) - if filename.startswith('../'): - raise NotFound() - filename = os.path.join(self.root_path, 'static', filename) - if not os.path.isfile(filename): - raise NotFound() - return send_file(filename, conditional=True) - def test_client(self): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. diff --git a/flask/helpers.py b/flask/helpers.py index 0afd4a85..518d953f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -11,6 +11,7 @@ import os import sys +import posixpath import mimetypes from time import time from zlib import adler32 @@ -330,6 +331,29 @@ class _PackageBoundObject(object): #: Where is the app root located? self.root_path = _get_package_path(self.import_name) + @property + def has_static_folder(self): + """This is `True` if the package bound object's container has a + folder named ``'static'``. + + .. versionadded:: 0.5 + """ + return os.path.isdir(os.path.join(self.root_path, 'static')) + + def send_static_file(self, filename): + """Function used internally to send static files from the static + folder to the browser. + + .. versionadded:: 0.5 + """ + filename = posixpath.normpath(filename) + if filename.startswith('../'): + raise NotFound() + filename = os.path.join(self.root_path, 'static', filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True) + def open_resource(self, resource): """Opens a resource from the application's resource folder. To see how this works, consider the following folder structure:: diff --git a/flask/module.py b/flask/module.py index 85898511..cc904d20 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,6 +12,27 @@ from flask.helpers import _PackageBoundObject +def _register_module_static(module): + """Internal helper function that returns a function for recording + that registers the `send_static_file` function for the module on + the application of necessary. + """ + def _register_static(state): + # do not register the rule if the static folder of the + # module is the same as the one from the application. + if state.app.root_path == module.root_path: + return + path = static_path + if path is None: + path = state.app.static_path + if state.url_prefix: + path = state.url_prefix + path + state.app.add_url_rule(path + '/', + '%s.static' % module.name, + view_func=module.send_static_file) + return _register_static + + class _ModuleSetupState(object): def __init__(self, app, url_prefix=None): @@ -67,7 +88,8 @@ class Module(_PackageBoundObject): :ref:`working-with-modules` section. """ - def __init__(self, import_name, name=None, url_prefix=None): + def __init__(self, import_name, name=None, url_prefix=None, + static_path=None): if name is None: assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' @@ -77,6 +99,10 @@ class Module(_PackageBoundObject): self.url_prefix = url_prefix self._register_events = [] + # if there is a static folder, register it for this module + if self.has_static_folder: + self._record(_register_module_static(self)) + def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. From a38dcd5e2bed1bcf2c1fc319fa9c3bc35e360fe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 13:42:00 +0200 Subject: [PATCH 0329/3747] Added support for loading templates from modules --- flask/app.py | 72 ++++++++++++++++++++++++++++++++++-------------- flask/helpers.py | 14 +++++++++- flask/module.py | 16 +++++------ 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/flask/app.py b/flask/app.py index a818ae62..fc46a4a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,7 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment, PackageLoader, FileSystemLoader +from jinja2 import Environment, BaseLoader, FileSystemLoader, TemplateNotFound from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -32,13 +32,38 @@ from flask.module import _ModuleSetupState _logger_lock = Lock() -def _select_autoescape(filename): - """Returns `True` if autoescaping should be active for the given - template name. +class _DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the module folders. """ - if filename is None: - return False - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + name = template + loader = None + try: + module, name = template.split('/', 1) + loader = self.app.modules[module].jinja_loader + except (ValueError, KeyError): + pass + if loader is None: + loader = self.app.jinja_loader + try: + return loader.get_source(environment, name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) + + def list_templates(self): + result = self.app.jinja_loader.list_templates() + for name, module in self.app.modules.iteritems(): + if module.jinja_loader is not None: + for template in module.jinja_loader.list_templates(): + result.append('%s/%s' % (name, template)) + return result class Flask(_PackageBoundObject): @@ -176,7 +201,6 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -245,6 +269,11 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } + #: all the loaded modules in a dictionary by name. + #: + #: .. versionadded:: 0.5 + self.modules = {} + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: @@ -269,8 +298,7 @@ class Flask(_PackageBoundObject): view_func=self.send_static_file) #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options` and the loader that is returned - #: by the :meth:`create_jinja_loader` function. + #: :attr:`jinja_options`. self.jinja_env = self.create_jinja_environment() self.init_jinja_globals() @@ -315,16 +343,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.5 """ - return Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - - def create_jinja_loader(self): - """Creates the Jinja loader. By default just a package loader for - the configured package is returned that looks up templates in the - `templates` folder. To add other loaders it's possible to - override this method. - """ - return FileSystemLoader(os.path.join(self.root_path, 'templates')) + options = dict(self.jinja_options) + if 'autoescape' not in options: + options['autoescape'] = self.select_jinja_autoescape + return Environment(loader=_DispatchingJinjaLoader(self), **options) def init_jinja_globals(self): """Called directly after the environment was created to inject @@ -339,6 +361,16 @@ class Flask(_PackageBoundObject): ) self.jinja_env.filters['tojson'] = _tojson_filter + def select_jinja_autoescape(self, filename): + """Returns `True` if autoescaping should be active for the given + template name. + + .. versionadded:: 0.5 + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/flask/helpers.py b/flask/helpers.py index 518d953f..d0b3ac23 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -27,7 +27,9 @@ except ImportError: except ImportError: json_available = False -from werkzeug import Headers, wrap_file, is_resource_modified +from werkzeug import Headers, wrap_file, is_resource_modified, cached_property + +from jinja2 import FileSystemLoader from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response @@ -340,6 +342,16 @@ class _PackageBoundObject(object): """ return os.path.isdir(os.path.join(self.root_path, 'static')) + @cached_property + def jinja_loader(self): + """The Jinja loader for this package bound object. + + .. versionadded:: 0.5 + """ + template_folder = os.path.join(self.root_path, 'templates') + if os.path.isdir(template_folder): + return FileSystemLoader(template_folder) + def send_static_file(self, filename): """Function used internally to send static files from the static folder to the browser. diff --git a/flask/module.py b/flask/module.py index cc904d20..ee1a342e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,12 +12,14 @@ from flask.helpers import _PackageBoundObject -def _register_module_static(module): +def _register_module(module): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on - the application of necessary. + the application of necessary. It also registers the module on + the application. """ - def _register_static(state): + def _register(state): + state.app.modules[module.name] = module # do not register the rule if the static folder of the # module is the same as the one from the application. if state.app.root_path == module.root_path: @@ -30,7 +32,7 @@ def _register_module_static(module): state.app.add_url_rule(path + '/', '%s.static' % module.name, view_func=module.send_static_file) - return _register_static + return _register class _ModuleSetupState(object): @@ -97,11 +99,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix - self._register_events = [] - - # if there is a static folder, register it for this module - if self.has_static_folder: - self._record(_register_module_static(self)) + self._register_events = [_register_module(self)] def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the From a3c9494f67a0e89d44f459dac9eb9d0bc9b9025b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 13:49:58 +0200 Subject: [PATCH 0330/3747] Moved templating stuff into a separate module --- flask/__init__.py | 4 +-- flask/app.py | 38 ++--------------------- flask/helpers.py | 25 --------------- flask/templating.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 flask/templating.py diff --git a/flask/__init__.py b/flask/__init__.py index 85c8dd3f..f6015a64 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,7 +18,7 @@ from jinja2 import Markup, escape from flask.app import Flask, Request, Response from flask.config import Config from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ - get_flashed_messages, render_template, render_template, render_template_string, \ - get_template_attribute, json + get_flashed_messages, get_template_attribute, json from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module +from flask.templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index fc46a4a3..725e1f4c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,8 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment, BaseLoader, FileSystemLoader, TemplateNotFound +from jinja2 import Environment + from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -27,45 +28,12 @@ from flask.ctx import _default_template_ctx_processor, _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession from flask.module import _ModuleSetupState +from flask.templating import _DispatchingJinjaLoader # a lock used for logger initialization _logger_lock = Lock() -class _DispatchingJinjaLoader(BaseLoader): - """A loader that looks for templates in the application and all - the module folders. - """ - - def __init__(self, app): - self.app = app - - def get_source(self, environment, template): - name = template - loader = None - try: - module, name = template.split('/', 1) - loader = self.app.modules[module].jinja_loader - except (ValueError, KeyError): - pass - if loader is None: - loader = self.app.jinja_loader - try: - return loader.get_source(environment, name) - except TemplateNotFound: - # re-raise the exception with the correct fileame here. - # (the one that includes the prefix) - raise TemplateNotFound(template) - - def list_templates(self): - result = self.app.jinja_loader.list_templates() - for name, module in self.app.modules.iteritems(): - if module.jinja_loader is not None: - for template in module.jinja_loader.list_templates(): - result.append('%s/%s' % (name, template)) - return result - - class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the diff --git a/flask/helpers.py b/flask/helpers.py index d0b3ac23..34c21cdd 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -290,31 +290,6 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv -def render_template(template_name, **context): - """Renders a template from the template folder with the given - context. - - :param template_name: the name of the template to be rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.get_template(template_name).render(context) - - -def render_template_string(source, **context): - """Renders a template from the given template source string - with the given context. - - :param template_name: the sourcecode of the template to be - rendered - :param context: the variables that should be available in the - context of the template. - """ - current_app.update_template_context(context) - return current_app.jinja_env.from_string(source).render(context) - - def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: diff --git a/flask/templating.py b/flask/templating.py new file mode 100644 index 00000000..0bb704c1 --- /dev/null +++ b/flask/templating.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +""" + flask.templating + ~~~~~~~~~~~~~~~~ + + Implements the bridge to Jinja2. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound + +from flask.globals import _request_ctx_stack + + +class _DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the module folders. + """ + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + name = template + loader = None + try: + module, name = template.split('/', 1) + loader = self.app.modules[module].jinja_loader + except (ValueError, KeyError): + pass + if loader is None: + loader = self.app.jinja_loader + try: + return loader.get_source(environment, name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) + + def list_templates(self): + result = self.app.jinja_loader.list_templates() + for name, module in self.app.modules.iteritems(): + if module.jinja_loader is not None: + for template in module.jinja_loader.list_templates(): + result.append('%s/%s' % (name, template)) + return result + + +def render_template(template_name, **context): + """Renders a template from the template folder with the given + context. + + :param template_name: the name of the template to be rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return ctx.app.jinja_env.get_template(template_name).render(context) + + +def render_template_string(source, **context): + """Renders a template from the given template source string + with the given context. + + :param template_name: the sourcecode of the template to be + rendered + :param context: the variables that should be available in the + context of the template. + """ + ctx = _request_ctx_stack.top + ctx.app.update_template_context(context) + return ctx.app.jinja_env.from_string(source).render(context) From 665fa2a32b5ff2b1c1887a48ed69329110a555f7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:01:56 +0200 Subject: [PATCH 0331/3747] More refactoring and moving stuff around --- flask/app.py | 6 +++--- flask/ctx.py | 11 ----------- flask/templating.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/flask/app.py b/flask/app.py index 725e1f4c..67b93cf7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,11 +24,12 @@ from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter from flask.wrappers import Request, Response from flask.config import ConfigAttribute, Config -from flask.ctx import _default_template_ctx_processor, _RequestContext +from flask.ctx import _RequestContext from flask.globals import _request_ctx_stack, request from flask.session import Session, _NullSession from flask.module import _ModuleSetupState -from flask.templating import _DispatchingJinjaLoader +from flask.templating import _DispatchingJinjaLoader, \ + _default_template_ctx_processor # a lock used for logger initialization _logger_lock = Lock() @@ -831,4 +832,3 @@ class Flask(_PackageBoundObject): def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) - diff --git a/flask/ctx.py b/flask/ctx.py index 1c538ecf..08eb1bf7 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -63,14 +63,3 @@ class _RequestContext(object): if not self.request.environ.get('flask._preserve_context') and \ (tb is None or not self.app.debug): self.pop() - -def _default_template_ctx_processor(): - """Default template context processor. Injects `request`, - `session` and `g`. - """ - reqctx = _request_ctx_stack.top - return dict( - request=reqctx.request, - session=reqctx.session, - g=reqctx.g - ) diff --git a/flask/templating.py b/flask/templating.py index 0bb704c1..3a4217dd 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -13,6 +13,18 @@ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from flask.globals import _request_ctx_stack +def _default_template_ctx_processor(): + """Default template context processor. Injects `request`, + `session` and `g`. + """ + reqctx = _request_ctx_stack.top + return dict( + request=reqctx.request, + session=reqctx.session, + g=reqctx.g + ) + + class _DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all the module folders. From d67a36cbdb20bfd9cd9931bceb8e6e3c27e9263f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:12:29 +0200 Subject: [PATCH 0332/3747] Added a testcase for the improved module support --- flask/module.py | 4 ++-- tests/flask_tests.py | 15 +++++++++++++++ tests/moduleapp/__init__.py | 7 +++++++ tests/moduleapp/apps/__init__.py | 0 tests/moduleapp/apps/admin/__init__.py | 9 +++++++++ tests/moduleapp/apps/admin/static/test.txt | 1 + tests/moduleapp/apps/admin/templates/index.html | 1 + tests/moduleapp/apps/frontend/__init__.py | 9 +++++++++ .../moduleapp/apps/frontend/templates/index.html | 1 + 9 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/moduleapp/__init__.py create mode 100644 tests/moduleapp/apps/__init__.py create mode 100644 tests/moduleapp/apps/admin/__init__.py create mode 100644 tests/moduleapp/apps/admin/static/test.txt create mode 100644 tests/moduleapp/apps/admin/templates/index.html create mode 100644 tests/moduleapp/apps/frontend/__init__.py create mode 100644 tests/moduleapp/apps/frontend/templates/index.html diff --git a/flask/module.py b/flask/module.py index ee1a342e..d106e69b 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,7 +12,7 @@ from flask.helpers import _PackageBoundObject -def _register_module(module): +def _register_module(module, static_path): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on the application of necessary. It also registers the module on @@ -99,7 +99,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix - self._register_events = [_register_module(self)] + self._register_events = [_register_module(self, static_path)] def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7e2c9246..384e2e91 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -630,6 +630,21 @@ class ModuleTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_templates_and_static(self): + from moduleapp import app + c = app.test_client() + + rv = c.get('/') + assert rv.data == 'Hello from the Frontend' + rv = c.get('/admin/') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/static/test.txt') + assert rv.data.strip() == 'Admin File' + + with app.test_request_context(): + assert flask.url_for('admin.static', filename='test.txt') \ + == '/admin/static/test.txt' + class SendfileTestCase(unittest.TestCase): diff --git a/tests/moduleapp/__init__.py b/tests/moduleapp/__init__.py new file mode 100644 index 00000000..35e82d4e --- /dev/null +++ b/tests/moduleapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from moduleapp.apps.admin import admin +from moduleapp.apps.frontend import frontend +app.register_module(admin) +app.register_module(frontend) diff --git a/tests/moduleapp/apps/__init__.py b/tests/moduleapp/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/moduleapp/apps/admin/__init__.py b/tests/moduleapp/apps/admin/__init__.py new file mode 100644 index 00000000..98af2b26 --- /dev/null +++ b/tests/moduleapp/apps/admin/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +admin = Module(__name__, url_prefix='/admin') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') diff --git a/tests/moduleapp/apps/admin/static/test.txt b/tests/moduleapp/apps/admin/static/test.txt new file mode 100644 index 00000000..f220d22f --- /dev/null +++ b/tests/moduleapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/tests/moduleapp/apps/admin/templates/index.html b/tests/moduleapp/apps/admin/templates/index.html new file mode 100644 index 00000000..eeec199a --- /dev/null +++ b/tests/moduleapp/apps/admin/templates/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/tests/moduleapp/apps/frontend/__init__.py b/tests/moduleapp/apps/frontend/__init__.py new file mode 100644 index 00000000..f83581e7 --- /dev/null +++ b/tests/moduleapp/apps/frontend/__init__.py @@ -0,0 +1,9 @@ +from flask import Module, render_template + + +frontend = Module(__name__) + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/tests/moduleapp/apps/frontend/templates/index.html b/tests/moduleapp/apps/frontend/templates/index.html new file mode 100644 index 00000000..a062d713 --- /dev/null +++ b/tests/moduleapp/apps/frontend/templates/index.html @@ -0,0 +1 @@ +Hello from the Frontend From f694456ff623941020da16839b7edd95248ed26e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:13:30 +0200 Subject: [PATCH 0333/3747] Documented changes --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 34cd23d8..64ec43ba 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,7 @@ Codename to be decided, release date to be announced. do conditional responses builtin. - (temporarily) dropped support for zipped applications. This was a rarely used feature and led to some confusing behaviour. +- added support for per-package template and static-file directories. Version 0.4 ----------- From 31d359694aeb4ef8770e7d2ba14ae479c23006a2 Mon Sep 17 00:00:00 2001 From: florentx Date: Sun, 4 Jul 2010 19:02:27 +0800 Subject: [PATCH 0334/3747] Fix typos --- docs/_themes | 1 - docs/unicode.rst | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 21cf0743..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 21cf07433147212ee6c8ab203dfa648a9239c66f diff --git a/docs/unicode.rst b/docs/unicode.rst index 7db462b8..5b4202ad 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -8,7 +8,7 @@ should probably read `The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets `_. This part of the documentation just tries to cover the very basics so that you have a -pleasent experience with unicode related things. +pleasant experience with unicode related things. Automatic Conversion -------------------- @@ -54,7 +54,7 @@ unicode. What does working with unicode in Python 2.x mean? UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of your Python source file. -- Jinja is configured to decode the template files from UTF08. So make +- Jinja is configured to decode the template files from UTF-8. So make sure to tell your editor to save the file as UTF-8 there as well. Encoding and Decoding Yourself @@ -63,12 +63,12 @@ Encoding and Decoding Yourself If you are talking with a filesystem or something that is not really based on unicode you will have to ensure that you decode properly when working with unicode interface. So for example if you want to load a file on the -filesystem and embedd it into a Jinja2 template you will have to decode it -form the encoding of that file. Here the old problem that textfiles do +filesystem and embed it into a Jinja2 template you will have to decode it +from the encoding of that file. Here the old problem that text files do not specify their encoding comes into play. So do yourself a favour and -limit yourself to UTF-8 for textfiles as well. +limit yourself to UTF-8 for text files as well. -Anyways. To load such a file with unicode you can use the builtin +Anyways. To load such a file with unicode you can use the built-in :meth:`str.decode` method:: def read_file(filename, charset='utf-8'): @@ -104,4 +104,4 @@ set your editor to store as UTF-8: 3. Select "UTF-8 without BOM" as encoding It is also recommended to use the Unix newline format, you can select - it in the same panel but this not a requirement. + it in the same panel but this is not a requirement. From b551f15b22c1d7d7749901e8d186246fb12038e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:17:29 +0200 Subject: [PATCH 0335/3747] Restored 2.5 compatibility{ --- flask/app.py | 2 ++ flask/config.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/flask/app.py b/flask/app.py index 67b93cf7..983dde8f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os from threading import Lock from datetime import timedelta, datetime diff --git a/flask/config.py b/flask/config.py index 7918de01..cf9ad541 100644 --- a/flask/config.py +++ b/flask/config.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os import sys From 2b00ec4017ee6b930bb805c294a459f112e4bc80 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:19:17 +0200 Subject: [PATCH 0336/3747] Small cleanups --- flask/app.py | 4 ++-- flask/helpers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flask/app.py b/flask/app.py index 983dde8f..c20fade9 100644 --- a/flask/app.py +++ b/flask/app.py @@ -363,9 +363,9 @@ class Flask(_PackageBoundObject): .. admonition:: Keep in Mind - Flask will supress any server error with a generic error page + Flask will suppress any server error with a generic error page unless it is in debug mode. As such to enable just the - interactive debugger without the code reloading, you ahve to + interactive debugger without the code reloading, you have to invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. Setting ``use_debugger`` to `True` without being in debug mode won't catch any exceptions because there won't be any to diff --git a/flask/helpers.py b/flask/helpers.py index 34c21cdd..3d23842c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -214,8 +214,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionadded:: 0.2 .. versionadded:: 0.5 - The `add_etags`, `cache_timeout` and `conditional` parameters were added. - The default behaviour is now to attach etags. + The `add_etags`, `cache_timeout` and `conditional` parameters were + added. The default behaviour is now to attach etags. :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a From 362cf493c42deaa417aa42a402852679ac349a4d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 14:20:20 +0200 Subject: [PATCH 0337/3747] Documented the create_jinja_loader removal --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 64ec43ba..32f889a3 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,8 @@ Codename to be decided, release date to be announced. - (temporarily) dropped support for zipped applications. This was a rarely used feature and led to some confusing behaviour. - added support for per-package template and static-file directories. +- removed support for `create_jinja_loader` which is no longer used + in 0.5 due to the improved module support. Version 0.4 ----------- From 9e1111c2fbc2c7670eb489df0262a90417b96019 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 17:21:13 +0200 Subject: [PATCH 0338/3747] Fixed JSON availability test. This fixes #77 --- flask/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index f6015a64..02141f56 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -17,8 +17,12 @@ from jinja2 import Markup, escape from flask.app import Flask, Request, Response from flask.config import Config -from flask.helpers import url_for, jsonify, json_available, flash, send_file, \ - get_flashed_messages, get_template_attribute, json +from flask.helpers import url_for, jsonify, json_available, flash, \ + send_file, get_flashed_messages, get_template_attribute from flask.globals import current_app, g, request, session, _request_ctx_stack from flask.module import Module from flask.templating import render_template, render_template_string + +# only import json if it's available +if json_available: + from flask.helpers import json From bca1acf1f38b0bb6d461fbbc486d6c95eafe0b30 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 19:57:53 +0200 Subject: [PATCH 0339/3747] Rewrote becoming big and foreword --- docs/becomingbig.rst | 100 ++++++++++++++++++++++++++----------------- docs/foreword.rst | 28 ++++++------ 2 files changed, 72 insertions(+), 56 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 8ad125de..916aa324 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -3,54 +3,74 @@ Becoming Big ============ -Your application is becoming more and more complex? Flask is really not -designed for large scale applications and does not attempt to do so, but -that does not mean you picked the wrong tool in the first place. +Your application is becoming more and more complex? If you suddenly +realize that Flask does things in a way that does not work out for your +application there are ways to deal with that. Flask is powered by Werkzeug and Jinja2, two libraries that are in use at a number of large websites out there and all Flask does is bring those -two together. Being a microframework, Flask is literally a single file. -What that means for large applications is that it's probably a good idea -to take the code from Flask and put it into a new module within the -applications and expand on that. +two together. Being a microframework Flask does not do much more than +combinding existing libraries - there is not a lot of code involved. +What that means for large applications is that it's very easy to take the +code from Flask and put it into a new module within the applications and +expand on that. -What Could Be Improved? ------------------------ +Flask is designed to be extended and modified in a couple of different +ways: -For instance it makes a lot of sense to change the way endpoints (the -names of the functions / URL rules) are handled to also take the module -name into account. Right now the function name is the URL name, but -imagine you have a large application consisting of multiple components. -In that case, it makes a lot of sense to use dotted names for the URL -endpoints. +- Subclassing. The majority of functionality can be changed by creating + a new subclass of the :class:`~flask.Flask` class and overriding + some methods. -Here are some suggestions for how Flask can be modified to better -accommodate large-scale applications: +- Flask extensions. For a lot of reusable functionality you can create + extensions. -- get rid of the decorator function registering which causes a lot - of troubles for applications that have circular dependencies. It - also requires that the whole application is imported when the system - initializes or certain URLs will not be available right away. A - better solution would be to have one module with all URLs in there and - specifying the target functions explicitly or by name and importing - them when needed. -- switch to explicit request object passing. This requires more typing - (because you now have something to pass around) but it makes it a - whole lot easier to debug hairy situations and to test the code. -- integrate the `Babel`_ i18n package or `SQLAlchemy`_ directly into the - core framework. +- Forking. If nothing else works out you can just take the Flask + codebase at a given point and copy/paste it into your application + and change it. Flask is designed with that in mind and makes this + incredible easy. You just have to take the package and copy it + into your application's code and rename it (for example to + `framework`). Then you can start modifying the code in there. -.. _Babel: http://babel.edgewall.org/ -.. _SQLAlchemy: http://www.sqlalchemy.org/ +Why consider Forking? +--------------------- -Why does Flask not do all that by Default? ------------------------------------------- +The majority of code of Flask is within Werkzeug and Jinja2. These +libraries do the majority of the work. Flask is just the paste that glues +those together. For every project there is the point where the underlying +framework gets in the way (due to assumptions the original developers +had). This is natural because if this would not be the case, the +framework would be a very complex system to begin with which causes a +steep learning curve and a lot of user frustration. -There is a huge difference between a small application that only has to -handle a couple of requests per second and with an overall code complexity -of less than 4000 lines of code and something of larger scale. At some -point it becomes important to integrate external systems, different -storage backends and more. +This is not unique to Flask. Many people use patched and modified +versions of their framework to counter shortcomings. This idea is also +reflected in the license of Flask. You don't have to contribute any +changes back if you decide to modify the framework. -If Flask was designed with all these contingencies in mind, it would be a -much more complex framework and harder to get started with. +The downside of forking is of course that Flask extensions will most +likely break because the new framework has a different import name and +because of that forking should be the last resort. + +Scaling like a Pro +------------------ + +For many web applications the complexity of the code is less an issue than +the scaling for the number of users or data entries expected. Flask by +itself is only limited in terms of scaling by your application code, the +data store you want to use and the Python implementation and webserver you +are running on. + +Scaling well means for example that if you double the amount of servers +you get about twice the performance. Scaling bad means that if you add a +new server the application won't perform any better or would not even +support a second server. + +There is only one limiting factor regarding scaling in Flask which are +the context local proxies. They depend on context which in Flask is +defined as being either a thread or a greenlet. Separate processes are +fine as well. If your server uses some kind of concurrency that is not +based on threads or greenlets, Flask will no longer be able to support +these global proxies. However the majority of servers are using either +threads, greenlets or separate processes to achieve concurrency which are +all methods well supported by the underlying Werkzeug library. diff --git a/docs/foreword.rst b/docs/foreword.rst index de6b4980..6b9d99b2 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -17,24 +17,20 @@ may be necessary in larger or more complex applications. For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in order to stay threadsafe. While this is a really easy approach and saves -you a lot of time, it also does not scale well to large applications. -It's especially painful for more complex unittests, and when you suddenly -have to deal with code being executed outside of the context of a request, -such as in cron jobs. +you a lot of time, it might also cause some troubles for very large +applications because changes on these thread-local objects can happen +anywhere in the same thread. -Flask provides some tools to deal with the downsides of this approach, but -the core problem remains. Flask is also based on convention over -configuration, which means that many things are preconfigured and will -work well for smaller applications but not so well for larger ones. For -example, by convention, templates and static files are in subdirectories -within the Python source tree of the application. +Flask provides some tools to deal with the downsides of this approach but +it might be an issue for larger applications. Flask is also based on +convention over configuration, which means that many things are +preconfigured and will work well for smaller applications but not so well +for larger ones. For example, by convention, templates and static files +are in subdirectories within the Python source tree of the application. -But don't worry if your application suddenly grows larger -and you're afraid Flask might not grow with it. Even with -larger frameworks, you'll eventually discover that you need -something the framework just cannot do for you without modification. -If you are ever in that situation, check out the :ref:`becomingbig` -chapter. +However Flask is not much code and built in a very solid foundation and +with that very easy to adapt for large applications. If you are +interested in that, check out the :ref:`becomingbig` chapter. A Framework and an Example -------------------------- From 80eb6cfffc3f3fe6e448aa0e26ba1566d4c42917 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:00:23 +0200 Subject: [PATCH 0340/3747] Switched to relative imports in the package --- flask/__init__.py | 14 +++++++------- flask/app.py | 16 ++++++++-------- flask/ctx.py | 4 ++-- flask/helpers.py | 4 ++-- flask/templating.py | 2 +- flask/wrappers.py | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 02141f56..76eed660 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -15,14 +15,14 @@ from werkzeug import abort, redirect from jinja2 import Markup, escape -from flask.app import Flask, Request, Response -from flask.config import Config -from flask.helpers import url_for, jsonify, json_available, flash, \ +from .app import Flask, Request, Response +from .config import Config +from .helpers import url_for, jsonify, json_available, flash, \ send_file, get_flashed_messages, get_template_attribute -from flask.globals import current_app, g, request, session, _request_ctx_stack -from flask.module import Module -from flask.templating import render_template, render_template_string +from .globals import current_app, g, request, session, _request_ctx_stack +from .module import Module +from .templating import render_template, render_template_string # only import json if it's available if json_available: - from flask.helpers import json + from .helpers import json diff --git a/flask/app.py b/flask/app.py index c20fade9..87f3e9d3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -22,15 +22,15 @@ from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound -from flask.helpers import _PackageBoundObject, url_for, get_flashed_messages, \ +from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter -from flask.wrappers import Request, Response -from flask.config import ConfigAttribute, Config -from flask.ctx import _RequestContext -from flask.globals import _request_ctx_stack, request -from flask.session import Session, _NullSession -from flask.module import _ModuleSetupState -from flask.templating import _DispatchingJinjaLoader, \ +from .wrappers import Request, Response +from .config import ConfigAttribute, Config +from .ctx import _RequestContext +from .globals import _request_ctx_stack, request +from .session import Session, _NullSession +from .module import _ModuleSetupState +from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor # a lock used for logger initialization diff --git a/flask/ctx.py b/flask/ctx.py index 08eb1bf7..90ea50db 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -11,8 +11,8 @@ from werkzeug.exceptions import HTTPException -from flask.globals import _request_ctx_stack -from flask.session import _NullSession +from .globals import _request_ctx_stack +from .session import _NullSession class _RequestGlobals(object): diff --git a/flask/helpers.py b/flask/helpers.py index 3d23842c..2303b58d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -31,8 +31,8 @@ from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from jinja2 import FileSystemLoader -from flask.globals import session, _request_ctx_stack, current_app, request -from flask.wrappers import Response +from .globals import session, _request_ctx_stack, current_app, request +from .wrappers import Response def _assert_have_json(): diff --git a/flask/templating.py b/flask/templating.py index 3a4217dd..d1e75959 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -10,7 +10,7 @@ """ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound -from flask.globals import _request_ctx_stack +from .globals import _request_ctx_stack def _default_template_ctx_processor(): diff --git a/flask/wrappers.py b/flask/wrappers.py index d962a563..7605ea13 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,7 +12,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property -from helpers import json +from .helpers import json class Request(RequestBase): From 51a89bf35ec6231d55f5caa8e12484e801f593fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:07:03 +0200 Subject: [PATCH 0341/3747] Restored 2.5 compatibility and actual fix for the json problem --- .gitignore | 2 ++ flask/helpers.py | 6 +++++- flask/module.py | 2 +- flask/wrappers.py | 3 +-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 148e5029..4844be8e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.pyc *.pyo env +env* dist +*.egg *.egg-info _mailinglist diff --git a/flask/helpers.py b/flask/helpers.py index 2303b58d..dcf25baf 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -19,6 +19,7 @@ from zlib import adler32 # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. json_available = True +json = None try: import simplejson as json except ImportError: @@ -32,7 +33,6 @@ from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from jinja2 import FileSystemLoader from .globals import session, _request_ctx_stack, current_app, request -from .wrappers import Response def _assert_have_json(): @@ -364,3 +364,7 @@ class _PackageBoundObject(object): subfolders use forward slashes as separator. """ return open(os.path.join(self.root_path, resource), 'rb') + + +# circular dependencies between wrappers and helpers +from .wrappers import Response diff --git a/flask/module.py b/flask/module.py index d106e69b..dfd30675 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ -from flask.helpers import _PackageBoundObject +from .helpers import _PackageBoundObject def _register_module(module, static_path): diff --git a/flask/wrappers.py b/flask/wrappers.py index 7605ea13..1dcf23d1 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,7 +12,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property -from .helpers import json +from .helpers import json, _assert_have_json class Request(RequestBase): @@ -52,7 +52,6 @@ class Request(RequestBase): parsed JSON data. """ if __debug__: - from flask.helpers import _assert_have_json _assert_have_json() if self.mimetype == 'application/json': return json.loads(self.data) From f1cde5bbfcb7f0466ad35511ffe2e31ea57756d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:11:54 +0200 Subject: [PATCH 0342/3747] Added more versionadded directives --- flask/app.py | 9 +++++++++ flask/module.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/flask/app.py b/flask/app.py index 87f3e9d3..9cdf9274 100644 --- a/flask/app.py +++ b/flask/app.py @@ -83,6 +83,15 @@ class Flask(_PackageBoundObject): up, that debugging information is lost. (For example it would only pick up SQL queries in `yourapplicaiton.app` and not `yourapplication.views.frontend`) + + .. versionadded:: 0.5 + The `static_path` parameter was added. + + :param import_name: the name of the application package + :param static_path: can be used to specify a different path for the + static files on the web. Defaults to ``/static``. + This does not affect the folder the files are served + *from*. """ #: The class that is used for request objects. See :class:`~flask.Request` diff --git a/flask/module.py b/flask/module.py index dfd30675..6a4f0fb3 100644 --- a/flask/module.py +++ b/flask/module.py @@ -88,6 +88,21 @@ class Module(_PackageBoundObject): For a gentle introduction into modules, checkout the :ref:`working-with-modules` section. + + .. versionadded:: 0.5 + The `static_path` parameter was added. + + :param import_name: the name of the Python package or module + implementing this :class:`Module`. + :param name: the internal short name for the module. Unless specified + the rightmost part of the import name + :param url_prefix: an optional string that is used to prefix all the + URL rules of this module. This can also be specified + when registering the module with the application. + :param static_path: can be used to specify a different path for the + static files on the web. Defaults to ``/static``. + This does not affect the folder the files are served + *from*. """ def __init__(self, import_name, name=None, url_prefix=None, From 8945a97a42d6475b1ee9fcb2689dc65dfce9828f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:36:34 +0200 Subject: [PATCH 0343/3747] fixed possible security problem in module branch --- flask/helpers.py | 3 ++- tests/flask_tests.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index dcf25baf..3387f623 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -29,6 +29,7 @@ except ImportError: json_available = False from werkzeug import Headers, wrap_file, is_resource_modified, cached_property +from werkzeug.exceptions import NotFound from jinja2 import FileSystemLoader @@ -334,7 +335,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ filename = posixpath.normpath(filename) - if filename.startswith('../'): + if filename.startswith(('/', '../')): raise NotFound() filename = os.path.join(self.root_path, 'static', filename) if not os.path.isfile(filename): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 384e2e91..b3835302 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -20,6 +20,7 @@ from logging import StreamHandler from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header +from werkzeug.exceptions import NotFound from cStringIO import StringIO example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -645,6 +646,25 @@ class ModuleTestCase(unittest.TestCase): assert flask.url_for('admin.static', filename='test.txt') \ == '/admin/static/test.txt' + def test_safe_access(self): + from moduleapp import app + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + rv = f('/etc/passwd') + except NotFound: + pass + else: + assert 0, 'expected exception' + try: + rv = f('../__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + class SendfileTestCase(unittest.TestCase): From 09e7b5eb2f3a3da0b09ffe4cd3cad8b028530874 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 20:40:58 +0200 Subject: [PATCH 0344/3747] Added a chapter for 0.5 in upgrading.rst --- docs/upgrading.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 20604c8c..7ee35176 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,30 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.5 +----------- + +Flask 0.5 is the first release that comes as a Python package instead of a +single module. There were a couple of internal refactoring so if you +depend on undocumented internal details you probably have to adapt the +imports. + +The following changes may be relevant to your application: + +- autoescaping no longer happens for all templates. Instead it is + configured to only happen on files ending with ``.html``, ``.htm``, + ``.xml`` and ``.xhtml``. If you have templates with different + extensions you should override the + :meth:`~flask.Flask.select_jinja_autoescape` method. +- Flask no longer supports zipped applications in this release. This + functionality might come back in future releases if there is demand + for this feature. Removing support for this makes the Flask internal + code easier to understand and fixes a couple of small issues that make + debugging harder than necessary. +- The `create_jinja_loader` function is gone. If you want to customize + the Jinja loader now, use the + :meth:`~flask.Flask.create_jinja_environment` method instead. + Version 0.4 ----------- From b7109865a352e6bbee55247731136a46e1bab04d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Jul 2010 22:57:49 +0200 Subject: [PATCH 0345/3747] Removed circular dependency by going over a proxy. This is the better solution anyway --- flask/helpers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 3387f623..8f2a2083 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -268,8 +268,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, file = open(filename, 'rb') data = wrap_file(request.environ, file) - rv = Response(data, mimetype=mimetype, headers=headers, - direct_passthrough=True) + rv = current_app.response_class(data, mimetype=mimetype, headers=headers, + direct_passthrough=True) rv.cache_control.public = True if cache_timeout: @@ -365,7 +365,3 @@ class _PackageBoundObject(object): subfolders use forward slashes as separator. """ return open(os.path.join(self.root_path, resource), 'rb') - - -# circular dependencies between wrappers and helpers -from .wrappers import Response From ac13deff401069c3854acca10c926119c6e1cbe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:23:35 +0200 Subject: [PATCH 0346/3747] Re-added support for folder with static files, refactored static file sending --- flask/__init__.py | 3 +- flask/app.py | 2 +- flask/helpers.py | 36 +++++++++++++++---- flask/module.py | 2 +- tests/flask_tests.py | 2 ++ .../moduleapp/apps/admin/static/css/test.css | 1 + 6 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 tests/moduleapp/apps/admin/static/css/test.css diff --git a/flask/__init__.py b/flask/__init__.py index 76eed660..f4617271 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -18,7 +18,8 @@ from jinja2 import Markup, escape from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ - send_file, get_flashed_messages, get_template_attribute + send_file, send_from_directory, get_flashed_messages, \ + get_template_attribute from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index 9cdf9274..8e559d4e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -273,7 +273,7 @@ class Flask(_PackageBoundObject): # if there is a static folder, register it for the application. if self.has_static_folder: - self.add_url_rule(self.static_path + '/', + self.add_url_rule(self.static_path + '/', endpoint='static', view_func=self.send_static_file) diff --git a/flask/helpers.py b/flask/helpers.py index 8f2a2083..65506698 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -291,6 +291,33 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv +def send_from_directory(directory, filename, **options): + """Send a file from a given directory with :func:`send_file`. This + is a secure way to quickly expose static files from an upload folder + or something similar. + + Example usage:: + + @app.route('/uploads/') + def download_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename, as_attachment=True) + + :param directory: the directory where all the files are stored. + :param filename: the filename relative to that directory to + download. + :param options: optional keyword arguments that are directly + forwarded to :func:`send_file`. + """ + filename = posixpath.normpath(filename) + if filename.startswith(('/', '../')): + raise NotFound() + filename = os.path.join(directory, filename) + if not os.path.isfile(filename): + raise NotFound() + return send_file(filename, conditional=True, **options) + + def _get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: @@ -334,13 +361,8 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - filename = posixpath.normpath(filename) - if filename.startswith(('/', '../')): - raise NotFound() - filename = os.path.join(self.root_path, 'static', filename) - if not os.path.isfile(filename): - raise NotFound() - return send_file(filename, conditional=True) + return send_from_directory(os.path.join(self.root_path, 'static'), + filename) def open_resource(self, resource): """Opens a resource from the application's resource folder. To see diff --git a/flask/module.py b/flask/module.py index 6a4f0fb3..7bebac4b 100644 --- a/flask/module.py +++ b/flask/module.py @@ -29,7 +29,7 @@ def _register_module(module, static_path): path = state.app.static_path if state.url_prefix: path = state.url_prefix + path - state.app.add_url_rule(path + '/', + state.app.add_url_rule(path + '/', '%s.static' % module.name, view_func=module.send_static_file) return _register diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b3835302..129ec3b2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -641,6 +641,8 @@ class ModuleTestCase(unittest.TestCase): assert rv.data == 'Hello from the Admin' rv = c.get('/admin/static/test.txt') assert rv.data.strip() == 'Admin File' + rv = c.get('/admin/static/css/test.css') + assert rv.data.strip() == '/* nested file */' with app.test_request_context(): assert flask.url_for('admin.static', filename='test.txt') \ diff --git a/tests/moduleapp/apps/admin/static/css/test.css b/tests/moduleapp/apps/admin/static/css/test.css new file mode 100644 index 00000000..b9f564de --- /dev/null +++ b/tests/moduleapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ From c34b03e9a677e467ea420d4286d4350ed429a4b8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:25:51 +0200 Subject: [PATCH 0347/3747] Documented send_from_directory --- CHANGES | 1 + docs/api.rst | 2 ++ flask/helpers.py | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 32f889a3..10ed8e9c 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,7 @@ Codename to be decided, release date to be announced. - added support for per-package template and static-file directories. - removed support for `create_jinja_loader` which is no longer used in 0.5 due to the improved module support. +- added a helper function to expose files from any directory. Version 0.4 ----------- diff --git a/docs/api.rst b/docs/api.rst index b6c81d10..d7f887e4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -230,6 +230,8 @@ Useful Functions and Classes .. autofunction:: send_file +.. autofunction:: send_from_directory + .. autofunction:: escape .. autoclass:: Markup diff --git a/flask/helpers.py b/flask/helpers.py index 65506698..560492c6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -303,6 +303,15 @@ def send_from_directory(directory, filename, **options): return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True) + .. admonition:: Sending files and Performance + + It is strongly recommended to activate either `X-Sendfile` support in + your webserver or (if no authentication happens) to tell the webserver + to serve files for the given path on its own without calling into the + web application for improved performance. + + .. versionadded:: 0.5 + :param directory: the directory where all the files are stored. :param filename: the filename relative to that directory to download. From df3f8940c30447f28f2ddf2a5f764cf543755f0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:31:18 +0200 Subject: [PATCH 0348/3747] Added separate module for testing --- flask/app.py | 25 +++---------------------- flask/ctx.py | 1 + flask/module.py | 2 +- flask/testing.py | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 flask/testing.py diff --git a/flask/app.py b/flask/app.py index 8e559d4e..dbc102f7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -18,7 +18,7 @@ from itertools import chain from jinja2 import Environment -from werkzeug import ImmutableDict, create_environ +from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -409,27 +409,7 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.4 added support for `with` block usage for the client. """ - from werkzeug import Client - class FlaskClient(Client): - preserve_context = context_preserved = False - def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False - kwargs.setdefault('environ_overrides', {}) \ - ['flask._preserve_context'] = self.preserve_context - old = _request_ctx_stack.top - try: - return Client.open(self, *args, **kwargs) - finally: - self.context_preserved = _request_ctx_stack.top is not old - def __enter__(self): - self.preserve_context = True - return self - def __exit__(self, exc_type, exc_value, tb): - self.preserve_context = False - if self.context_preserved: - _request_ctx_stack.pop() + from flask.testing import FlaskClient return FlaskClient(self, self.response_class, use_cookies=True) def open_session(self, request): @@ -838,6 +818,7 @@ class Flask(_PackageBoundObject): :func:`werkzeug.create_environ` for more information, this function accepts the same arguments). """ + from werkzeug import create_environ return self.request_context(create_environ(*args, **kwargs)) def __call__(self, environ, start_response): diff --git a/flask/ctx.py b/flask/ctx.py index 90ea50db..5ceb1750 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -60,6 +60,7 @@ class _RequestContext(object): # exception happened. This will allow the debugger to still # access the request object in the interactive shell. Furthermore # the context can be force kept alive for the test client. + # See flask.testing for how this works. if not self.request.environ.get('flask._preserve_context') and \ (tb is None or not self.app.debug): self.pop() diff --git a/flask/module.py b/flask/module.py index 7bebac4b..df2b5b40 100644 --- a/flask/module.py +++ b/flask/module.py @@ -30,7 +30,7 @@ def _register_module(module, static_path): if state.url_prefix: path = state.url_prefix + path state.app.add_url_rule(path + '/', - '%s.static' % module.name, + endpoint='%s.static' % module.name, view_func=module.send_static_file) return _register diff --git a/flask/testing.py b/flask/testing.py new file mode 100644 index 00000000..ee973e0a --- /dev/null +++ b/flask/testing.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" + flask.testing + ~~~~~~~~~~~~~ + + Implements test support helpers. This module is lazily imported + and usually not used in production environments. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from werkzeug import Client +from flask import _request_ctx_stack + + +class FlaskClient(Client): + + preserve_context = context_preserved = False + + def open(self, *args, **kwargs): + if self.context_preserved: + _request_ctx_stack.pop() + self.context_preserved = False + kwargs.setdefault('environ_overrides', {}) \ + ['flask._preserve_context'] = self.preserve_context + old = _request_ctx_stack.top + try: + return Client.open(self, *args, **kwargs) + finally: + self.context_preserved = _request_ctx_stack.top is not old + + def __enter__(self): + self.preserve_context = True + return self + + def __exit__(self, exc_type, exc_value, tb): + self.preserve_context = False + if self.context_preserved: + _request_ctx_stack.pop() From e5d82020384ae527c36424b52612bbe625db9268 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:32:06 +0200 Subject: [PATCH 0349/3747] Added a docstring --- flask/testing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/testing.py b/flask/testing.py index ee973e0a..fe7f8c2d 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -14,6 +14,11 @@ from flask import _request_ctx_stack class FlaskClient(Client): + """Works like a regular Werkzeug test client but has some + knowledge about how Flask works to defer the cleanup of the + request context stack to the end of a with body when used + in a with statement. + """ preserve_context = context_preserved = False From 77e2fbf249031a7adb7e745f15ad5645027b5994 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 10:37:44 +0200 Subject: [PATCH 0350/3747] Added a separate logging module --- flask/app.py | 19 +++---------------- flask/logging.py | 42 ++++++++++++++++++++++++++++++++++++++++++ flask/testing.py | 1 + 3 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 flask/logging.py diff --git a/flask/app.py b/flask/app.py index dbc102f7..e5691edd 100644 --- a/flask/app.py +++ b/flask/app.py @@ -300,22 +300,9 @@ class Flask(_PackageBoundObject): with _logger_lock: if self._logger and self._logger.name == self.logger_name: return self._logger - from logging import getLogger, StreamHandler, Formatter, \ - Logger, DEBUG - class DebugLogger(Logger): - def getEffectiveLevel(x): - return DEBUG if self.debug else Logger.getEffectiveLevel(x) - class DebugHandler(StreamHandler): - def emit(x, record): - StreamHandler.emit(x, record) if self.debug else None - handler = DebugHandler() - handler.setLevel(DEBUG) - handler.setFormatter(Formatter(self.debug_log_format)) - logger = getLogger(self.logger_name) - logger.__class__ = DebugLogger - logger.addHandler(handler) - self._logger = logger - return logger + from flask.logging import create_logger + self._logger = rv = create_logger(self) + return rv def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` diff --git a/flask/logging.py b/flask/logging.py new file mode 100644 index 00000000..4ccd8b1b --- /dev/null +++ b/flask/logging.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" + flask.logging + ~~~~~~~~~~~~~ + + Implements the logging support for Flask. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import absolute_import + +from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG + + +def create_logger(app): + """Creates a logger for the given application. This logger works + similar to a regular Python logger but changes the effective logging + level based on the application's debug flag. Furthermore this + function also removes all attached handlers in case there was a + logger with the log name before. + """ + + class DebugLogger(Logger): + def getEffectiveLevel(x): + return DEBUG if app.debug else Logger.getEffectiveLevel(x) + + class DebugHandler(StreamHandler): + def emit(x, record): + StreamHandler.emit(x, record) if app.debug else None + + handler = DebugHandler() + handler.setLevel(DEBUG) + handler.setFormatter(Formatter(app.debug_log_format)) + logger = getLogger(app.logger_name) + # just in case that was not a new logger, get rid of all the handlers + # already attached to it. + del logger.handlers[:] + logger.__class__ = DebugLogger + logger.addHandler(handler) + return logger diff --git a/flask/testing.py b/flask/testing.py index fe7f8c2d..ee4bd28e 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -9,6 +9,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + from werkzeug import Client from flask import _request_ctx_stack From 56796f0f439a5c0e00d7b3d73a662a2a8e11e7c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Jul 2010 11:06:29 +0200 Subject: [PATCH 0351/3747] More doc changes regarding foreword --- docs/foreword.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 6b9d99b2..b43fe870 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -10,9 +10,10 @@ What does "micro" mean? To me, the "micro" in microframework refers not only to the simplicity and small size of the framework, but also to the typically limited complexity -and size of applications that are written with the framework. To be -approachable and concise, a microframework sacrifices a few features that -may be necessary in larger or more complex applications. +and size of applications that are written with the framework. Also the +fact that you can have an entire application in a single Python file. To +be approachable and concise, a microframework sacrifices a few features +that may be necessary in larger or more complex applications. For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in @@ -72,13 +73,13 @@ So always keep security in mind when doing web development. Target Audience --------------- -Is Flask for you? If your application is small-ish and does not depend on -very complex database structures, Flask is the Framework for you. It was -designed from the ground up to be easy to use, and built on the firm -foundation of established principles, good intentions, and mature, widely -used libraries. Recent versions of Flask scale nicely within reasonable -bounds, and if you grow larger, you won't have any trouble adjusting Flask -for your new application size. +Is Flask for you? If your application is small or medium sized and does +not depend on very complex database structures, Flask is the Framework for +you. It was designed from the ground up to be easy to use, and built on +the firm foundation of established principles, good intentions, and +mature, widely used libraries. Recent versions of Flask scale nicely +within reasonable bounds, and if you grow larger, you won't have any +trouble adjusting Flask for your new application size. If you suddenly discover that your application grows larger than originally intended, head over to the :ref:`becomingbig` section to see From da514b398429653dbd368c6da48c9863d3c2632f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 10:42:36 +0200 Subject: [PATCH 0352/3747] Respect the domain for the session cookie. This fixes #79 --- flask/app.py | 7 +++++-- tests/flask_tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index e5691edd..0bdce818 100644 --- a/flask/app.py +++ b/flask/app.py @@ -420,11 +420,14 @@ class Flask(_PackageBoundObject): object) :param response: an instance of :attr:`response_class` """ - expires = None + expires = domain = None if session.permanent: expires = datetime.utcnow() + self.permanent_session_lifetime + if self.config['SERVER_NAME'] is not None: + domain = '.' + self.config['SERVER_NAME'] session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True) + expires=expires, httponly=True, + domain=domain) def register_module(self, module, **options): """Registers a module with this application. The keyword argument diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 129ec3b2..1da3b23c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -172,6 +172,20 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.post('/set', data={'value': '42'}).data == 'value set' assert c.get('/get').data == '42' + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + assert 'domain=.example.com' in rv.headers['set-cookie'].lower() + assert 'httponly' in rv.headers['set-cookie'].lower() + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): From 60e143ecb63df6d820dacb1ac6416e0c73714524 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 10:43:38 +0200 Subject: [PATCH 0353/3747] Documented change regarding the session cookie domain policy in the CHANGES --- CHANGES | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 10ed8e9c..1fdc0d86 100644 --- a/CHANGES +++ b/CHANGES @@ -10,7 +10,8 @@ Codename to be decided, release date to be announced. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with - the `SERVER_NAME` config key. + the `SERVER_NAME` config key. This key is now also used to set + the session cookie cross-subdomain wide. - autoescaping is no longer active for all templates. Instead it is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. Inside templates this behaviour can be changed with the From 77743a5f15ddbc7d826abd4f189217bef88991b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 12:33:39 +0200 Subject: [PATCH 0354/3747] Some documentation improvements --- docs/patterns/packages.rst | 46 ++++++++++++++++++++++++++++++++++++++ flask/module.py | 4 +++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index db5b00a3..cb372f2e 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -197,3 +197,49 @@ did in the example above, or we just use the function name:: @frontend.route('/') def index(): return "I'm the index" + +.. _modules-and-resources: + +Modules and Resources +--------------------- + +.. versionadded:: 0.5 + +If a module is located inside an actual Python package it may contain +static files and templates. Imagine you have an application like this:: + + + /yourapplication + __init__.py + /apps + /frontend + __init__.py + views.py + /static + style.css + /templates + index.html + about.html + ... + /admin + __init__.py + views.py + /static + style.css + /templates + list_items.html + show_item.html + ... + +The static folders automatically become exposed as URLs. For example if +the `admin` module is exported with an URL prefix of ``/admin`` you can +access the style css from its static folder by going to +``/admin/static/style.css``. The URL endpoint for the static files of the +admin would be ``'admin.static'``, similar to how you refer to the regular +static folder of the whole application as ``'static'``. + +If you want to refer to the templates you just have to prefix it with the +name of the module. So for the admin it would be +``render_template('admin/list_items.html')`` and so on. It is not +possible to refer to templates without the prefixed modlue name. This is +explicit unlike URL rules. diff --git a/flask/module.py b/flask/module.py index df2b5b40..b30020c5 100644 --- a/flask/module.py +++ b/flask/module.py @@ -90,7 +90,9 @@ class Module(_PackageBoundObject): :ref:`working-with-modules` section. .. versionadded:: 0.5 - The `static_path` parameter was added. + The `static_path` parameter was added and it's now possible for + modules to refer to their own templates and static files. See + :ref:`modules-and-resources` for more information. :param import_name: the name of the Python package or module implementing this :class:`Module`. From b7c0e564d4c8ddd82550f2c2958672558c1c657b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 14:42:43 +0200 Subject: [PATCH 0355/3747] Added a chapter on configuration options --- docs/config.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index fabf6dc4..522d5d73 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -132,3 +132,70 @@ experience: 2. Do not write code that needs the configuration at import time. If you limit yourself to request-only accesses to the configuration you can reconfigure the object later on as needed. + + +Development / Production +------------------------ + +Most applications need more than one configuration. There will at least +be a separate configuration for a production server and one used during +development. The easiest way to handle this is to use a default +configuration that is always loaded and part of version control, and a +separate configuration that overrides the values as necessary as mentioned +in the example above:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_settings') + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + +Then you just have to add a separate `config.py` file and export +``YOURAPPLICATION_SETTINGS=/path/to/config.py`` and you are done. However +there are alternative ways as well. For example you could use imports or +subclassing. + +What is very popular in the Django world is to make the import explicit in +the config file by adding an ``from yourapplication.default_settings +import *`` to the top of the file and then overriding the changes by hand. +You could also inspect an environment variable like +``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc +and import different hardcoded files based on that. + +An interesting pattern is also to use classes and inheritance for +configuration:: + + class Config(object): + DEBUG = False + TESTING = False + DATABASE_URI = 'sqlite://:memory:' + + class ProductionConfig(Config): + DATABASE_URI = 'mysql://user@localhost/foo' + + class DevelopmentConfig(Config): + DEBUG = True + + class TestinConfig(Config): + TESTING = True + +To enable such a config you just have to call into +:meth:`~flask.Config.from_object`:: + + app.config.from_object('configmodule.ProductionConfig') + +There are many different ways and it's up to you how you want to manage +your configuration files. However here a list of good recommendations:: + +- keep a default configuration in version control. Either populate the + config with this default configuration or import it in your own + configuration files before overriding values. +- use an environment variable to switch between the configurations. + This can be done from outside the Python interpreter and makes + development and deployment much easier because you can quickly and + easily switch between different configs without having to touch the + code at all. If you are working often on different projects you can + even create your own script for sourcing that activates a virtualenv + and exports the development configuration for you. +- Use a tool like `fabric`_ in production to push code and + configurations sepearately to the production server(s). + +.. _fabric: http://fabfile.org/ From 34fcd19306dca6359d351bb987b686ca12bb2ab6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:07:13 +0200 Subject: [PATCH 0356/3747] Added chapter about fabric based deployments --- docs/config.rst | 5 +- docs/deploying/mod_wsgi.rst | 15 +++ docs/patterns/distribute.rst | 4 + docs/patterns/fabric.rst | 196 +++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 docs/patterns/fabric.rst diff --git a/docs/config.rst b/docs/config.rst index 522d5d73..0e8a24cd 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -183,7 +183,7 @@ To enable such a config you just have to call into app.config.from_object('configmodule.ProductionConfig') There are many different ways and it's up to you how you want to manage -your configuration files. However here a list of good recommendations:: +your configuration files. However here a list of good recommendations: - keep a default configuration in version control. Either populate the config with this default configuration or import it in your own @@ -196,6 +196,7 @@ your configuration files. However here a list of good recommendations:: even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and - configurations sepearately to the production server(s). + configurations sepearately to the production server(s). For some + details about how to do that, head over to the :ref:`deploy` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index b2d25f5c..5b19f1d5 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -1,3 +1,5 @@ +.. _mod_wsgi-deployment: + mod_wsgi (Apache) ================= @@ -134,6 +136,19 @@ If your application does not run, follow this guide to troubleshoot: filename is used to locate the resources and for symlinks the wrong filename is picked up. +Support for Automatic Reloading +------------------------------- + +To help deployment tools you can activate support for automatic reloading. +Whenever something changes the `.wsgi` file, `mod_wsgi` will reload all +the daemon processes for us. + +For that, just add the following directive to your `Directory` section: + +.. sourcecode:: apache + + WSGIScriptReloading On + Working with Virtual Environments --------------------------------- diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index a3217f6e..b6f6a5ef 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -33,6 +33,10 @@ not supported by `distribute`_ so we will not bother with it. If you have not yet converted your application into a package, head over to the :ref:`larger-applications` pattern to see how this can be done. +A working deployment with distribute is the first step into more complex +and more automated deployment scenarios. If you want to fully automate +the process, also read the :ref:`fabric-deployment` chapter. + Basic Setup Script ------------------ diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst new file mode 100644 index 00000000..5f306cec --- /dev/null +++ b/docs/patterns/fabric.rst @@ -0,0 +1,196 @@ +.. _fabric-deployment: + +Deploying with Fabric +===================== + +`Fabric`_ is a tool for Python similar to Makefiles but with the ability +to execute commands on a remote server. In combination with a properly +set up Python package (:ref:`larger-applications`) and a good concept for +configurations (:ref:`config`) it is very easy to deploy Flask +applications to external servers. + +Before we get started, here a quick checklist of things we have to ensure +upfront: + +- Fabric 1.0 has to be installed locally. This tutorial assumes the + latest version of Fabric. +- The application already has to be a package and requires a working + `setup.py` file (:ref:`distribute-deployment`). +- In the following example we are using `mod_wsgi` for the remote + servers. You can of course use your own favourite server there, but + for this example we chose Apache + `mod_wsgi` because it's very easy + to setup and has a simple way to reload applications without root + access. + +Creating the first Fabfile +-------------------------- + +A fabfile is what controls what Fabric executes. It is named `fabfile.py` +and executed by the `fab` command. All the functions defined in that file +will show up as `fab` subcommands. They are executed on one or more +hosts. These hosts can be defined either in the fabfile or on the command +line. In this case we will add them to the fabfile. + +This is a basic first example that has the ability to upload the current +sourcecode to the server and install it into a already existing +virtual environment:: + + from fabric.api import * + + # the user to use for the remote commands + env.user = 'appuser' + # the servers where the commands are executed + env.hosts = ['server1.example.com', 'server2.example.com'] + + def pack(): + # create a new source distribution as tarball + local('python setup.py sdist --formats=gztar', capture=False) + + def deploy(): + # figure out the release name and version + dist = local('python setup.py --fullname').strip() + # upload the source tarball to the temporary folder on the server + put('sdist/%s.tar.gz' % dist, '/tmp/') + # create a place where we can unzip the tarball, then enter + # that directory and unzip it + run('mkdir yourapplication') + with cd('/tmp/yourapplication'): + run('tar xzf /tmp/yourapplication.tar.gz') + # now setup the package with our virtual environment's + # python interpreter + run('/var/www/yourapplication/env/bin/python setup.py install') + # now that all is set up, delete the folder again + run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz') + # and finally touch the .wsgi file so that mod_wsgi triggers + # a reload of the application + run('touch /var/www/yourapplication.wsgi') + +The example above is well documented and should be straightforward. Here +a recap of the most common commands fabric provides: + +- `run` - executes a command on a remote server +- `local` - executes a command on the local machine +- `put` - uploads a file to the remote server +- `cd` - changes the directory on the serverside. This has to be used + in combination with the `with` statement. + +Running Fabfiles +---------------- + +Now how do you execute that fabfile? You use the `fab` command. To +deploy the current version of the code on the remote server you would use +this command:: + + $ fab pack deploy + +However this requires that our server already has the +``/var/www/yourapplication`` folder created and +``/var/www/yourapplication/env`` to be a virtual environment. Furthermore +are we not creating the configuration or `.wsgi` file on the server. So +how do we bootstrap a new server into our infrastructure? + +This now depends on the number of servers we want to set up. If we just +have one application server (which the majority of applications will +have), creating a command in the fabfile for this is overkill. But +obviously you can do that. In that case you would probably call it +`setup` or `bootstrap` and then pass the servername explicitly on the +command line:: + + $ fab -H newserver.example.com bootstrap + +To setup a new server you would roughly do these steps: + +1. Create the directory structure in ``/var/www``:: + + $ mkdir /var/www/yourapplication + $ cd /var/www/yourapplication + $ virtualenv --distribute env + +2. Upload a new `application.wsgi` file to the server and the + configuration file for the application (eg: `application.cfg`) + +3. Create a new Apache config for `yourapplication` and activate it. + Make sure to activate watching for changes of the `.wsgi` file so + that we can automatically reload the application by touching it. + (See :ref:`mod_wsgi-deployment` for more information) + +So now the question is, where do the `application.wsgi` and +`application.cfg` files come from? + +The WSGI File +------------- + +The WSGI file has to import the application and also to set an environment +variable so that the application knows where to look for the config. This +is a short example that does exactly that:: + + import os + os.environ['YOURAPPLICATION_CONFIG'] = '/var/www/yourapplication/application.cfg' + from yourapplication import app + +The application itself then has to initialize itself like this to look for +the config at that environment variable:: + + app = Flask(__name__) + app.config.from_object('yourapplication.default_config') + app.config.from_envvar('YOURAPPLICATION_CONFIG') + +This approach is explained in detail in the :ref:`config` section of the +documentation. + +The Configuration File +---------------------- + +Now as mentioned above, the application will find the correct +configuration file by looking up the `YOURAPPLICATION_CONFIG` environment +variable. So we have to put the configuration in a place where the +application will able to find it. Configuration files have the unfriendly +quality of being different on all computers, so you do not version them +usually. + +A popular approach is to store configuration files for different servers +in a separate version control repository and check them out on all +servers. Then symlink the file that is active for the server into the +location where it's expected (eg: ``/var/www/yourapplication``). + +Either way, in our case here we only expect one or two servers and we can +upload them ahead of time by hand. + +First Deployment +---------------- + +Now we can do our first deployment. We have set up the servers so that +they have their virtual environments and activated apache configs. Now we +can pack up the application and deploy it:: + + $ fab pack deploy + +Fabric will now connect to all servers and run the commands as written +down in the fabfile. First it will execute pack so that we have our +tarball ready and then it will execute deploy and upload the source code +to all servers and install it there. Thanks to the `setup.py` file we +will automatically pull in the required libraries into our virtual +environment. + +Next Steps +---------- + +From that point onwards there is so much that can be done to make +deployment actually fun: + +- Create a `bootstrap` command that initializes new servers. It could + initialize a new virtual environment, setup apache appropriately etc. +- Put configuration files into a separate version control repository + and symlink the active configs into place. +- You could also put your application code into a repository and check + out the latest version on the server and then install. That way you + can also easily go back to older versions. +- hook in testing functionality so that you can deploy to an external + server and run the testsuite. + +Working with Fabric is fun and you will notice that it's quite magical to +type ``fab deploy`` and see your application being deployed automatically +to one or more remote servers. + + +.. _Fabric: http://fabfile.org/ diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 23b20dc5..21f3a562 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -19,6 +19,7 @@ Snippet Archives `_. packages appfactories distribute + fabric sqlite3 sqlalchemy fileuploads From 5dd4e9f318e3bb27967d86f42be64fa839967145 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:10:49 +0200 Subject: [PATCH 0357/3747] Fixed a typo --- docs/patterns/fabric.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index 5f306cec..de23ffa8 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -50,7 +50,7 @@ virtual environment:: # figure out the release name and version dist = local('python setup.py --fullname').strip() # upload the source tarball to the temporary folder on the server - put('sdist/%s.tar.gz' % dist, '/tmp/') + put('sdist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it run('mkdir yourapplication') From 4c937be2524de0fddc2d2f7f39b09677497260aa Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:25:44 +0200 Subject: [PATCH 0358/3747] Preparing for release --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 1fdc0d86..68b42d94 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.5 ----------- -Codename to be decided, release date to be announced. +Released on July 6th 2010, codename Calvados. - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with From 3f7ab1797730b8d07497744878d109baa9b235a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 16:28:47 +0200 Subject: [PATCH 0359/3747] HEAD is 0.6-dev --- CHANGES | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 68b42d94..0e58331a 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.6 +----------- + +Release date to be announced, codename to be decided. + Version 0.5 ----------- diff --git a/setup.py b/setup.py index dd3561da..b97a33f7 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.5', + version='0.6', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 98ea6579c80fc7754eef433e3d6a0f232b8be75a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:10:08 +0200 Subject: [PATCH 0360/3747] Preparing for a 0.5.1 bugfix release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd3561da..6e7005bf 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.5', + version='0.5.1', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 596ae0ced530adef7021c134b3b89e74154d7eb6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:11:51 +0200 Subject: [PATCH 0361/3747] Updated changelog --- CHANGES | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 68b42d94..e554c701 100644 --- a/CHANGES +++ b/CHANGES @@ -3,10 +3,18 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.5.1 +------------- + +Bugfix Release, released on July 6th 2010 + +- fixes an issue with template loading for modules + + Version 0.5 ----------- -Released on July 6th 2010, codename Calvados. +Released on July 6th 2010, codename Calvados - fixed a bug with subdomains that was caused by the inability to specify the server name. The server name can now be set with @@ -48,7 +56,7 @@ Released on June 18th 2010, codename Rakia Version 0.3.1 ------------- -Bugfix release, released May 28th 2010 +Bugfix release, released on May 28th 2010 - fixed a error reporting bug with :meth:`flask.Config.from_envvar` - removed some unused code from flask From 0a93c552cc3318396e44c6ab1282190898ce0012 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 19:24:50 +0200 Subject: [PATCH 0362/3747] Fixed a template lookup error --- CHANGES | 3 ++- flask/templating.py | 22 ++++++++++++---------- tests/flask_tests.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index e554c701..103a66f3 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,8 @@ Version 0.5.1 Bugfix Release, released on July 6th 2010 -- fixes an issue with template loading for modules +- fixes an issue with template loading from directories when modules + where used. Version 0.5 diff --git a/flask/templating.py b/flask/templating.py index d1e75959..06d8be04 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -34,21 +34,23 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): - name = template - loader = None try: module, name = template.split('/', 1) loader = self.app.modules[module].jinja_loader + if loader is None: + raise ValueError() except (ValueError, KeyError): - pass - if loader is None: loader = self.app.jinja_loader - try: - return loader.get_source(environment, name) - except TemplateNotFound: - # re-raise the exception with the correct fileame here. - # (the one that includes the prefix) - raise TemplateNotFound(template) + if loader is not None: + return loader.get_source(environment, template) + else: + try: + return loader.get_source(environment, name) + except TemplateNotFound: + pass + # raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) def list_templates(self): result = self.app.jinja_loader.list_templates() diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1da3b23c..5b4f34ef 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -21,6 +21,7 @@ from contextlib import contextmanager from datetime import datetime from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound from cStringIO import StringIO example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -662,6 +663,17 @@ class ModuleTestCase(unittest.TestCase): assert flask.url_for('admin.static', filename='test.txt') \ == '/admin/static/test.txt' + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + assert e.name == 'missing.html' + else: + assert 0, 'expected exception' + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template('nested/nested.txt') == 'I\'m nested' + def test_safe_access(self): from moduleapp import app From 16e4d5a6553ff9c89a61c44ddec7da21126b631a Mon Sep 17 00:00:00 2001 From: florentx Date: Wed, 7 Jul 2010 02:07:11 +0800 Subject: [PATCH 0363/3747] Various typos. --- docs/patterns/packages.rst | 2 +- flask/app.py | 5 ++--- flask/logging.py | 2 +- flask/module.py | 2 +- flask/session.py | 2 +- flask/templating.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index cb372f2e..63d7d76e 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -241,5 +241,5 @@ static folder of the whole application as ``'static'``. If you want to refer to the templates you just have to prefix it with the name of the module. So for the admin it would be ``render_template('admin/list_items.html')`` and so on. It is not -possible to refer to templates without the prefixed modlue name. This is +possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. diff --git a/flask/app.py b/flask/app.py index 0bdce818..d16613b7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -31,7 +31,7 @@ from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ - _default_template_ctx_processor + _default_template_ctx_processor # a lock used for logger initialization _logger_lock = Lock() @@ -426,8 +426,7 @@ class Flask(_PackageBoundObject): if self.config['SERVER_NAME'] is not None: domain = '.' + self.config['SERVER_NAME'] session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True, - domain=domain) + expires=expires, httponly=True, domain=domain) def register_module(self, module, **options): """Registers a module with this application. The keyword argument diff --git a/flask/logging.py b/flask/logging.py index 4ccd8b1b..29caadce 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -11,7 +11,7 @@ from __future__ import absolute_import -from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG +from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG def create_logger(app): diff --git a/flask/module.py b/flask/module.py index b30020c5..8935359e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -15,7 +15,7 @@ from .helpers import _PackageBoundObject def _register_module(module, static_path): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on - the application of necessary. It also registers the module on + the application if necessary. It also registers the module on the application. """ def _register(state): diff --git a/flask/session.py b/flask/session.py index 324fc98d..df2d8773 100644 --- a/flask/session.py +++ b/flask/session.py @@ -37,7 +37,7 @@ class _NullSession(Session): def _fail(self, *args, **kwargs): raise RuntimeError('the session is unavailable because no secret ' 'key was set. Set the secret_key on the ' - 'application to something unique and secret') + 'application to something unique and secret.') __setitem__ = __delitem__ = clear = pop = popitem = \ update = setdefault = _fail del _fail diff --git a/flask/templating.py b/flask/templating.py index 06d8be04..23c55c2b 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -48,7 +48,7 @@ class _DispatchingJinjaLoader(BaseLoader): return loader.get_source(environment, name) except TemplateNotFound: pass - # raise the exception with the correct fileame here. + # raise the exception with the correct filename here. # (the one that includes the prefix) raise TemplateNotFound(template) From f1a6fbe250ccf5407b45a398d05bf63bc99c3105 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 6 Jul 2010 20:28:48 +0200 Subject: [PATCH 0364/3747] send_from_directory > SharedDataMiddleware --- docs/patterns/fileuploads.rst | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 1174ebfc..f4a9a1db 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -28,8 +28,6 @@ bootstrapping code for our application:: ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) app = Flask(__name__) - app.add_url_rule('/uploads/', 'uploaded_file', - build_only=True) So first we need a couple of imports. Most should be straightforward, the :func:`werkzeug.secure_filename` is explained a little bit later. The @@ -100,14 +98,23 @@ before storing it directly on the filesystem. >>> secure_filename('../../../../home/username/.bashrc') 'home_username_.bashrc' -Now if we run that application, you will notice that uploading works, but -you won't actually see that uploaded file. Well, you would have to -configure the server to serve that file for you. This is not handy for -development situations or when you are just too lazy to properly set up -the server. Would be nice to have the files still be available in that -situation, and that is really easy to do, just hook in a middleware:: +Now one last thing is missing: the serving of the uploaded files. As of +Flask 0.5 we can use a function that does that for us:: + + from flask import send_from_directory + + @app.route('/uploads/') + def uploaded_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], + filename) + +Alternatively you can register `uploaded_file` as `build_only` rule and +use the :class:`~werkzeug.SharedDataMiddleware`. This also works with +older versions of Flask:: from werkzeug import SharedDataMiddleware + app.add_url_rule('/uploads/', 'uploaded_file', + build_only=True) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { '/uploads': UPLOAD_FOLDER }) From 0198313a22a98361947fceb8544c355be676f784 Mon Sep 17 00:00:00 2001 From: Thomas Schranz Date: Thu, 8 Jul 2010 00:10:29 +0800 Subject: [PATCH 0365/3747] added workaround for json on Google AppEngine Google AppEngine unfortunately does not offer json/simplejson where we would expect it to be, but since they do offer django and django comes with simplejson as a frozen dependency, we can import it from there. prior art: http://github.com/facebook/python-sdk/blob/master/src/facebook.py#L50 --- flask/helpers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 560492c6..420bfab8 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -26,7 +26,12 @@ except ImportError: try: import json except ImportError: - json_available = False + try: + # Google Appengine offers simplejson via django + from django.utils import simplejson as json + except ImportError: + json_available = False + from werkzeug import Headers, wrap_file, is_resource_modified, cached_property from werkzeug.exceptions import NotFound From f3b6d94bf732a913aaf2005ad2656ee31ef8f6c6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 7 Jul 2010 09:19:41 -0700 Subject: [PATCH 0366/3747] added Thomas Schranz to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index e20a3735..c2415977 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,4 +24,5 @@ Patches and Suggestions - Sebastien Estienne - Simon Sapin - Stephane Wirtel +- Thomas Schranz - Zhao Xiaohong From d12d73263f5d1664a65a42f71f19158b7e07ef2c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 18:04:10 +0200 Subject: [PATCH 0367/3747] Reverse order of execution of post-request handlers. This fixes #82 --- CHANGES | 3 +++ docs/upgrading.rst | 11 +++++++++++ flask/app.py | 8 ++++++-- tests/flask_tests.py | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 840d1a3d..c09b9e04 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Version 0.6 Release date to be announced, codename to be decided. +- after request functions are now called in reverse order of + registration. + Version 0.5.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7ee35176..f3a7a27d 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,17 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.6 +----------- + +Flask 0.6 comes with a backwards incompatible change which affects the +order of after-request handlers. Previously they were called in the order +of the registration, now they are called in reverse order. This change +was made so that Flask behaves more like people expected it to work and +how other systems handle request pre- and postprocessing. If you +dependend on the order of execution of post-request functions, be sure to +change the order. + Version 0.5 ----------- diff --git a/flask/app.py b/flask/app.py index d16613b7..49852e5e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -707,6 +707,10 @@ class Flask(_PackageBoundObject): before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + :param response: a :attr:`response_class` object. :return: a new response object or the same, has to be an instance of :attr:`response_class`. @@ -717,9 +721,9 @@ class Flask(_PackageBoundObject): self.save_session(ctx.session, response) funcs = () if mod and mod in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[mod]) + funcs = reversed(self.after_request_funcs[mod]) if None in self.after_request_funcs: - funcs = chain(funcs, self.after_request_funcs[None]) + funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) return response diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5b4f34ef..380ecdad 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -318,6 +318,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Internal Server Error' in rv.data assert len(called) == 1 + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after1(response): + called.append(3) + return response + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + assert rv.data == '42' + assert called == [1, 2, 3, 4] + def test_error_handling(self): app = flask.Flask(__name__) @app.errorhandler(404) From 5e1b1030e8edecb0c9652f2a406933c049ea2397 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 23:04:24 +0200 Subject: [PATCH 0368/3747] Added support for automagic OPTIONS --- CHANGES | 3 +++ docs/quickstart.rst | 8 +++++++- flask/app.py | 37 +++++++++++++++++++++++++++++++------ flask/ctx.py | 5 +++-- flask/helpers.py | 1 + flask/wrappers.py | 21 ++++++++++++++++----- tests/flask_tests.py | 17 +++++++++++++---- 7 files changed, 74 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index c09b9e04..0c8f965e 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,9 @@ Release date to be announced, codename to be decided. - after request functions are now called in reverse order of registration. +- OPTIONS is now automatically implemented by Flask unless the + application explictly adds 'OPTIONS' as method to the URL rule. + In this case no automatic OPTIONS handling kicks in. Version 0.5.1 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e1fdce51..4e98f858 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -269,7 +269,8 @@ If `GET` is present, `HEAD` will be added automatically for you. You don't have to deal with that. It will also make sure that `HEAD` requests are handled like the `HTTP RFC`_ (the document describing the HTTP protocol) demands, so you can completely ignore that part of the HTTP -specification. +specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you +as well automatically. You have no idea what an HTTP method is? Worry not, here quick introduction in HTTP methods and why they matter: @@ -310,6 +311,11 @@ very common: `DELETE` Remove the information that the given location. +`OPTIONS` + Provides a quick way for a requesting client to figure out which + methods are supported by this URL. Starting with Flask 0.6, this + is implemented for you automatically. + Now the interesting part is that in HTML4 and XHTML1, the only methods a form might submit to the server are `GET` and `POST`. But with JavaScript and future HTML standards you can use other methods as well. Furthermore diff --git a/flask/app.py b/flask/app.py index 49852e5e..02f22aa3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -464,6 +464,9 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.2 `view_func` parameter added. + .. versionchanged:: 0.6 + `OPTIONS` is added automatically as method. + :param rule: the URL rule as string :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as @@ -471,15 +474,27 @@ class Flask(_PackageBoundObject): :param view_func: the function to call when serving a request to the provided endpoint :param options: the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. """ if endpoint is None: assert view_func is not None, 'expected view func if endpoint ' \ 'is not provided.' endpoint = view_func.__name__ options['endpoint'] = endpoint - options.setdefault('methods', ('GET',)) - self.url_map.add(Rule(rule, **options)) + methods = options.pop('methods', ('GET',)) + provide_automatic_options = False + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + rule = Rule(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) if view_func is not None: self.view_functions[endpoint] = view_func @@ -539,8 +554,10 @@ class Flask(_PackageBoundObject): :param rule: the URL rule as string :param methods: a list of methods this rule should be limited - to (``GET``, ``POST`` etc.). By default a rule - just listens for ``GET`` (and implicitly ``HEAD``). + to (`GET`, `POST` etc.). By default a rule + just listens for `GET` (and implicitly `HEAD`). + Starting with Flask 0.6, `OPTIONS` is implicitly + added and handled by the standard request handling. :param subdomain: specifies the rule for the subdomain in case subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes @@ -650,7 +667,15 @@ class Flask(_PackageBoundObject): try: if req.routing_exception is not None: raise req.routing_exception - return self.view_functions[req.endpoint](**req.view_args) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if rule.provide_automatic_options and req.method == 'OPTIONS': + rv = self.response_class() + rv.allow.update(rule.methods) + return rv + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) diff --git a/flask/ctx.py b/flask/ctx.py index 5ceb1750..854503af 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -38,8 +38,9 @@ class _RequestContext(object): self.flashes = None try: - self.request.endpoint, self.request.view_args = \ - self.url_adapter.match() + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule except HTTPException, e: self.request.routing_exception = e diff --git a/flask/helpers.py b/flask/helpers.py index 420bfab8..cf00528e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,6 +15,7 @@ import posixpath import mimetypes from time import time from zlib import adler32 +from functools import wraps # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. diff --git a/flask/wrappers.py b/flask/wrappers.py index 1dcf23d1..200e7caf 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -24,11 +24,12 @@ class Request(RequestBase): :attr:`~flask.Flask.request_class` to your subclass. """ - #: the endpoint that matched the request. This in combination with - #: :attr:`view_args` can be used to reconstruct the same or a - #: modified URL. If an exception happened when matching, this will - #: be `None`. - endpoint = None + #: the internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: + #: .. versionadded:: 0.6 + url_rule = None #: a dict of view arguments that matched the request. If an exception #: happened when matching, this will be `None`. @@ -40,6 +41,16 @@ class Request(RequestBase): #: something similar. routing_exception = None + @property + def endpoint(self): + """The endpoint that matched the request. This in combination with + :attr:`view_args` can be used to reconstruct the same or a + modified URL. If an exception happened when matching, this will + be `None`. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + @property def module(self): """The name of the current module""" diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 380ecdad..6c16bbd4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -111,6 +111,15 @@ class ContextTestCase(unittest.TestCase): class BasicFunctionalityTestCase(unittest.TestCase): + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + assert rv.data == '' + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') @@ -124,7 +133,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -132,7 +141,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_url_mapping(self): app = flask.Flask(__name__) @@ -148,7 +157,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/').data == 'GET' rv = c.post('/') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] rv = c.head('/') assert rv.status_code == 200 assert not rv.data # head truncates @@ -156,7 +165,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/more').data == 'GET' rv = c.delete('/more') assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] def test_session(self): app = flask.Flask(__name__) From 6e52355eb303e1b47f7b83ddbb94ff432e2df139 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 12 Jul 2010 23:58:43 +0200 Subject: [PATCH 0369/3747] tiny performance improvement --- flask/wrappers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 200e7caf..b0747564 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -54,8 +54,8 @@ class Request(RequestBase): @property def module(self): """The name of the current module""" - if self.endpoint and '.' in self.endpoint: - return self.endpoint.rsplit('.', 1)[0] + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.rsplit('.', 1)[0] @cached_property def json(self): From ed16ae2183ad44a7ba89aedd331a180455ed0836 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:14:53 +0200 Subject: [PATCH 0370/3747] Always register URL rules. This fixes #81 --- CHANGES | 4 ++++ flask/app.py | 13 ++++++++----- flask/helpers.py | 4 +--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index 0c8f965e..db26bdc7 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Release date to be announced, codename to be decided. - OPTIONS is now automatically implemented by Flask unless the application explictly adds 'OPTIONS' as method to the URL rule. In this case no automatic OPTIONS handling kicks in. +- static rules are now even in place if there is no static folder + for the module. This was implemented to aid GAE which will + remove the static folder if it's part of a mapping in the .yml + file. Version 0.5.1 ------------- diff --git a/flask/app.py b/flask/app.py index 02f22aa3..a70a930d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -271,11 +271,14 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() - # if there is a static folder, register it for the application. - if self.has_static_folder: - self.add_url_rule(self.static_path + '/', - endpoint='static', - view_func=self.send_static_file) + # register the static folder for the application. Do that even + # if the folder does not exist. First of all it might be created + # while the server is running (usually happens during development) + # but also because google appengine stores static files somewhere + # else when mapped with the .yml file. + self.add_url_rule(self.static_path + '/', + endpoint='static', + view_func=self.send_static_file) #: The Jinja2 environment. It is created from the #: :attr:`jinja_options`. diff --git a/flask/helpers.py b/flask/helpers.py index cf00528e..99a001dc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -366,9 +366,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - template_folder = os.path.join(self.root_path, 'templates') - if os.path.isdir(template_folder): - return FileSystemLoader(template_folder) + return FileSystemLoader(os.path.join(self.root_path, 'templates')) def send_static_file(self, filename): """Function used internally to send static files from the static From aa3d8398fd1a38b449b64fea4979f358f3c711e3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:30:29 +0200 Subject: [PATCH 0371/3747] Config is now available in templates, context processors no longer override keys --- CHANGES | 4 ++++ docs/upgrading.rst | 8 ++++++++ flask/app.py | 11 ++++++++++- flask/templating.py | 1 + tests/flask_tests.py | 24 ++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index db26bdc7..0c04a91d 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,10 @@ Release date to be announced, codename to be decided. for the module. This was implemented to aid GAE which will remove the static folder if it's part of a mapping in the .yml file. +- the :attr:`~flask.Flask.config` is now available in the templates + as `config`. +- context processors will no longer override values passed directly + to the render function. Version 0.5.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f3a7a27d..747fdb72 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -30,6 +30,14 @@ how other systems handle request pre- and postprocessing. If you dependend on the order of execution of post-request functions, be sure to change the order. +Another change that breaks backwards compatibility is that context +processors will no longer override values passed directly to the template +rendering function. If for example `request` is as variable passed +directly to the template, the default context processor will not override +it with the current request object. This makes it easier to extend +context processors later to inject additional variables without breaking +existing template not expecting them. + Version 0.5 ----------- diff --git a/flask/app.py b/flask/app.py index a70a930d..fd97bab4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -343,7 +343,11 @@ class Flask(_PackageBoundObject): def update_template_context(self, context): """Update the template context with some commonly used variables. - This injects request, session and g into the template context. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overriden if a context processor + decides to return a value with the same key. :param context: the context as a dictionary that is updated in place to add extra variables. @@ -352,8 +356,13 @@ class Flask(_PackageBoundObject): mod = _request_ctx_stack.top.request.module if mod is not None and mod in self.template_context_processors: funcs = chain(funcs, self.template_context_processors[mod]) + orig_ctx = context.copy() for func in funcs: context.update(func()) + # make sure the original values win. This makes it possible to + # easier add new variables in context processors without breaking + # existing views. + context.update(orig_ctx) def run(self, host='127.0.0.1', port=5000, **options): """Runs the application on a local development server. If the diff --git a/flask/templating.py b/flask/templating.py index 23c55c2b..c55d4826 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -19,6 +19,7 @@ def _default_template_ctx_processor(): """ reqctx = _request_ctx_stack.top return dict( + config=reqctx.app.config, request=reqctx.request, session=reqctx.session, g=reqctx.g diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6c16bbd4..533afd21 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -492,6 +492,30 @@ class TemplatingTestCase(unittest.TestCase): rv = app.test_client().get('/') assert rv.data == '

    23|42' + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + assert rv.data == '42' + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + assert rv.data.split() == ['42', '23', 'False', 'aha'] + def test_escaping(self): text = '

    Hello World!' app = flask.Flask(__name__) From 85ff63c32e7237280bff3293481a371fb3da180c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 13 Jul 2010 23:52:55 +0200 Subject: [PATCH 0372/3747] Emit correct date. In theory --- flask/helpers.py | 11 +++++++++++ tests/flask_tests.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 99a001dc..d76090c4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -240,6 +240,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, :param conditional: set to `True` to enable conditional responses. :param cache_timeout: the timeout in seconds for the headers. """ + mtime = None if isinstance(filename_or_fp, basestring): filename = filename_or_fp file = None @@ -272,11 +273,21 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, else: if file is None: file = open(filename, 'rb') + mtime = os.path.getmtime(filename) data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, direct_passthrough=True) + # if we know the file modification date, we can store it as the + # current time to better support conditional requests. Werkzeug + # as of 0.6.1 will override this value however in the conditional + # response with the current time. This will be fixed in Werkzeug + # with a new release, however many WSGI servers will still emit + # a separate date header. + if mtime is not None: + rv.date = int(mtime) + rv.cache_control.public = True if cache_timeout: rv.cache_control.max_age = cache_timeout diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 533afd21..b3b68b37 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -19,7 +19,7 @@ import tempfile from logging import StreamHandler from contextlib import contextmanager from datetime import datetime -from werkzeug import parse_date, parse_options_header +from werkzeug import parse_date, parse_options_header, http_date from werkzeug.exceptions import NotFound from jinja2 import TemplateNotFound from cStringIO import StringIO From f8f8463f3a0e260604b066de56b0a8c7d3a86db4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Jul 2010 02:50:41 +0200 Subject: [PATCH 0373/3747] Documented cookie problem for #80 --- docs/config.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/config.rst b/docs/config.rst index 0e8a24cd..7d5bd6fc 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -64,6 +64,25 @@ The following configuration values are used internally by Flask: subdomain support (eg: ``'localhost'``) =============================== ========================================= +.. admonition:: More on ``SERVER_NAME`` + + The ``SERVER_NAME`` key is used for the subdomain support. Because + Flask cannot guess the subdomain part without the knowledge of the + actual server name, this is required if you want to work with + subdomains. This is also used for the session cookie. + + Please keep in mind that not only Flask has the problem of not knowing + what subdomains are, your web browser does as well. Most modern web + browsers will not allow cross-subdomain cookies to be set on a + server name without dots in it. So if your server name is + ``'localhost'`` you will not be able to set a cookie for + ``'localhost'`` and every subdomain of it. Please chose a different + server name in that case, like ``'myapplication.local'`` and add + this name + the subdomains you want to use into your host config + or setup a local `bind`_. + +.. _bind: https://www.isc.org/software/bind + .. versionadded:: 0.4 ``LOGGER_NAME`` From b1790cca55956c0cda132ac2fa6a2fb53fd2d009 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 14 Jul 2010 10:47:57 +0200 Subject: [PATCH 0374/3747] Added MAX_CONTENT_LENGTH config key --- CHANGES | 2 ++ docs/config.rst | 6 ++++++ flask/app.py | 3 ++- flask/wrappers.py | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 0c04a91d..df24ab9d 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,8 @@ Release date to be announced, codename to be decided. as `config`. - context processors will no longer override values passed directly to the render function. +- added the ability to limit the incoming request data with the + new ``MAX_CONTENT_LENGTH`` configuration value. Version 0.5.1 ------------- diff --git a/docs/config.rst b/docs/config.rst index 7d5bd6fc..ab1923ba 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -62,6 +62,10 @@ The following configuration values are used internally by Flask: ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name of the server. Required for subdomain support (eg: ``'localhost'``) +``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will + reject incoming requests with a + content length greater than this by + returning a 413 status code. =============================== ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -89,6 +93,8 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.5 ``SERVER_NAME`` +.. versionadded:: ``MAX_CONTENT_LENGTH`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index fd97bab4..415bf753 100644 --- a/flask/app.py +++ b/flask/app.py @@ -193,7 +193,8 @@ class Flask(_PackageBoundObject): 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, - 'SERVER_NAME': None + 'SERVER_NAME': None, + 'MAX_CONTENT_LENGTH': None }) def __init__(self, import_name, static_path=None): diff --git a/flask/wrappers.py b/flask/wrappers.py index b0747564..c578170c 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -13,6 +13,7 @@ from werkzeug import Request as RequestBase, Response as ResponseBase, \ cached_property from .helpers import json, _assert_have_json +from .globals import _request_ctx_stack class Request(RequestBase): @@ -41,6 +42,13 @@ class Request(RequestBase): #: something similar. routing_exception = None + @property + def max_content_length(self): + """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.app.config['MAX_CONTENT_LENGTH'] + @property def endpoint(self): """The endpoint that matched the request. This in combination with From f5b8c082847baa8a902cef22c43a5d50d7a55bdc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:35:02 +0200 Subject: [PATCH 0375/3747] endpoint is optional for modules. This fixes #86 --- flask/app.py | 6 ++---- flask/helpers.py | 9 +++++++++ flask/module.py | 15 ++++++++++++--- tests/flask_tests.py | 12 ++++++++++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/flask/app.py b/flask/app.py index 415bf753..16da7939 100644 --- a/flask/app.py +++ b/flask/app.py @@ -23,7 +23,7 @@ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter + _tojson_filter, _endpoint_from_view_func from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import _RequestContext @@ -496,9 +496,7 @@ class Flask(_PackageBoundObject): added and handled by the standard request handling. """ if endpoint is None: - assert view_func is not None, 'expected view func if endpoint ' \ - 'is not provided.' - endpoint = view_func.__name__ + endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint methods = options.pop('methods', ('GET',)) provide_automatic_options = False diff --git a/flask/helpers.py b/flask/helpers.py index d76090c4..fc309e67 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -59,6 +59,15 @@ else: _tojson_filter = json.dumps +def _endpoint_from_view_func(view_func): + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, 'expected view func if endpoint ' \ + 'is not provided.' + return view_func.__name__ + + def jsonify(*args, **kwargs): """Creates a :class:`~flask.Response` with the JSON representation of the given arguments with an `application/json` mimetype. The arguments diff --git a/flask/module.py b/flask/module.py index 8935359e..9eaa4f82 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ -from .helpers import _PackageBoundObject +from .helpers import _PackageBoundObject, _endpoint_from_view_func def _register_module(module, static_path): @@ -127,15 +127,24 @@ class Module(_PackageBoundObject): return f return decorator - def add_url_rule(self, rule, endpoint, view_func=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. + + .. versionchanged:: 0.6 + The `endpoint` argument is now optional and will default to the + function name to consistent with the function of the same name + on the application object. """ def register_rule(state): the_rule = rule if state.url_prefix: the_rule = state.url_prefix + rule - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, endpoint), + the_endpoint = endpoint + if the_endpoint is None: + the_endpoint = _endpoint_from_view_func(view_func) + state.app.add_url_rule(the_rule, '%s.%s' % (self.name, + the_endpoint), view_func, **options) self._record(register_rule) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b3b68b37..f2451223 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -612,6 +612,18 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/admin/login').data == 'admin login' assert c.get('/admin/logout').data == 'admin logout' + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + assert rv.data == 'Awesome' + with app.test_request_context(): + assert flask.url_for('frontend.index') == '/' + def test_request_processing(self): catched = [] app = flask.Flask(__name__) From 4aeb44567ab46951a534fa2836b392ed21b1532b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:35:44 +0200 Subject: [PATCH 0376/3747] Documented change --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index df24ab9d..9e95b349 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,9 @@ Release date to be announced, codename to be decided. to the render function. - added the ability to limit the incoming request data with the new ``MAX_CONTENT_LENGTH`` configuration value. +- the endpoint for the :meth:`flask.Module.add_url_rule` method + is now optional to be consistent with the function of the + same name on the application object. Version 0.5.1 ------------- From 54ebd88f451cf58b7d07781692fafc465c94305f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 14:39:49 +0200 Subject: [PATCH 0377/3747] Documented MAX_CONTENT_LENGTH in the file upload docs and linked to Flask-Uploads --- docs/patterns/fileuploads.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index f4a9a1db..884925cf 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -125,27 +125,29 @@ If you now run the application everything should work as expected. Improving Uploads ----------------- +.. versionadded:: 0.6 + So how exactly does Flask handle uploads? Well it will store them in the webserver's memory if the files are reasonable small otherwise in a temporary location (as returned by :func:`tempfile.gettempdir`). But how do you specify the maximum file size after which an upload is aborted? By default Flask will happily accept file uploads to an unlimited amount of -memory, but you can limit that by subclassing the request and overriding -the Werkzeug provided :attr:`~werkzeug.BaseRequest.max_form_memory_size` -attribute:: +memory, but you can limit that by setting the `MAX_CONTENT_LENGTH` +config key:: from flask import Flask, Request - class LimitedRequest(Request): - max_form_memory_size = 16 * 1024 * 1024 - app = Flask(__name__) - app.request_class = LimitedRequest + app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 The code above will limited the maximum allowed payload to 16 megabytes. If a larger file is transmitted, Flask will raise an :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception. +This feature was added in Flask 0.6 but can be achieved in older versions +as well by subclassing the request object. For more information on that +consult the Werkzeug documentation on file handling. + Upload Progress Bars -------------------- @@ -165,3 +167,14 @@ following libraries for some nice examples how to do that: - `Plupload `_ - HTML5, Java, Flash - `SWFUpload `_ - Flash - `JumpLoader `_ - Java + + +An Easier Solution +------------------ + +Because the common pattern for file uploads exists almost unchanged in all +applications dealing with uploads, there is a Flask extension called +`Flask-Uploads`_ that implements a full fledged upload mechanism with +white and blacklisting of extensions and more. + +.. _Flask-Uploads: http://packages.python.org/Flask-Uploads/ From bc8e021b34a696660410ee60e61664ab969e5cf3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 15:42:27 +0200 Subject: [PATCH 0378/3747] Added a note on references to static folders --- docs/patterns/packages.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 63d7d76e..f6e1ad87 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -243,3 +243,18 @@ name of the module. So for the admin it would be ``render_template('admin/list_items.html')`` and so on. It is not possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. + +.. admonition:: References to Static Folders + + Please keep in mind that if you are using unqualified endpoints by + default Flask will always assume the module's static folder, even if + there is no such folder. + + If you want to refer to the application's static folder, use a leading + dot:: + + # this refers to the application's static folder + url_for('.static', filename='static.css') + + # this refers to the current module's static folder + url_for('static', filename='static.css') From e84ca8ed94d36e57f7ab8a547096ce7f61c06f5c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 15:43:17 +0200 Subject: [PATCH 0379/3747] Clearified that --- docs/patterns/packages.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index f6e1ad87..cd2deac1 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -258,3 +258,8 @@ explicit unlike URL rules. # this refers to the current module's static folder url_for('static', filename='static.css') + + This is the case for all endpoints, not just static folders, but for + static folders it's more common that you will stumble upon this because + most applications will have a static folder in the application and not + a specific module. From 793a56bc8b3a42601f0d3e9b6a1f0f73a9c31c33 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 17:01:04 +0200 Subject: [PATCH 0380/3747] Added the pocoo styleguide to the documentation --- docs/contents.rst.inc | 1 + docs/styleguide.rst | 200 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 docs/styleguide.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index e924202e..27d0b73a 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -44,6 +44,7 @@ Design notes, legal information and changelog are here for the interested. security unicode extensiondev + styleguide upgrading changelog license diff --git a/docs/styleguide.rst b/docs/styleguide.rst new file mode 100644 index 00000000..bb99a79a --- /dev/null +++ b/docs/styleguide.rst @@ -0,0 +1,200 @@ +Pocoo Styleguide +================ + +The Pocoo styleguide is the styleguide for all Pocoo Projects, including +Flask. This styleguide is a requirement for Patches to Flask and a +recommendation for Flask extensions. + +In general the Pocoo Styleguide closely follows :pep:`8` with some small +differences and extensions. + +General Layout +-------------- + +Indentation: + 4 real spaces. No tabs, no exceptions. + +Maximum line length: + 79 characters with a soft limit for 84 if absolutely necessary. Try + to avoid too nested code by cleverly placing `break`, `continue` and + `return` statements. + +Continuing long statements: + To continue a statement you can use backslashes in which case you should + align the next line with the last dot or equal sign, or indent four + spaces:: + + this_is_a_very_long(function_call, 'with many parameters') \ + .that_returns_an_object_with_an_attribute + + MyModel.query.filter(MyModel.scalar > 120) \ + .order_by(MyModel.name.desc()) \ + .limit(10) + + If you break in a statement with parentheses or brances, align to the + braces:: + + this_is_a_very_long(function_call, 'with many parameters', + 23, 42, 'and even more') + + For lists or tuples with many items, break immediately after the + opening brace:: + + items = [ + 'this is the first', 'set of items', 'with more items', + 'to come in this line', 'like this' + ] + +Blank lines: + Top level functions and classes are separated by two lines, everything + else by one. Do not use too many blank lines to separate logical + segments in code. Example:: + + def hello(name): + print 'Hello %s!' % name + + + def goodbye(name): + print 'See you %s.' % name + + + class MyClass(object): + """This is a simple docstring""" + + def __init__(self, name): + self.name = name + + def get_annoying_name(self): + return self.name.upper() + '!!!!111' + +Expressions and Statements +-------------------------- + +General whitespace rules: + - Whitespace is absend for unary operators that are not works + (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. + - Whitespace is placed between binary operators. + + Good:: + + exp = -1.05 + value = (item_value / item_count) * offset / exp + value = my_list[index] + value = my_dict['key'] + + Bad:: + + exp = - 1.05 + value = ( item_value / item_count ) * offset / exp + value = (item_value/item_count)*offset/exp + value=( item_value/item_count ) * offset/exp + value = my_list[ index ] + value = my_dict ['key'] + +Yoda statements are a nogo: + Never compare constant with variable, always variable with constant: + + Good:: + + if method == 'md5': + pass + + Bad:: + + if 'md5' == method: + pass + +Comparisons: + - against arbitary types: ``==`` and ``!=`` + - against singletones with ``is`` and ``is not`` (eg: ``foo is not + None``) + - never compare something with `True` or `False` (for example never + do ``foo == False``, do ``not foo`` instead) + +Negated containment checks: + use ``foo not in bar`` instead of ``not foo in bar`` + +Instance checks: + ``isinstance(a, C)`` instead of ``type(A) is C``, but try to avoid + instance checks in general. Check for features. + + +Naming Conventions +------------------ + +- Class names: ``CamelCase``, with acronyms kept uppercase (``HTTPWriter`` + and not ``HttpWriter``) +- Variable names: ``lowercase_with_underscores`` +- Method and functin names: ``lowercase_with_underscores`` +- Constants: ``UPPERCASE_WITH_UNDERSCORES`` +- precompiled regular expressions: ``name_re`` + +Protected members are prefixed with a single underscore. Double +underscores are reserved for mixin classes. + +On classes with keywords, trailing underscores are appended. Clashes with +builtins are allowed and **must not** be resolved by appending an +underline to the variable name. If the function needs to access a +shadowed builtin, rebind the builtin to a different name instead. + +Function and method arguments: + - class methods: ``cls`` as first parameter + - instance methods: ``self`` as first parameter + - lambdas for properties might have the first parameter replaced + with ``x`` like in ``display_name = property(lambda x: x.real_name + or x.username)`` + + +Docstrings +---------- + +Docstring conventions: + All docstrings are formatted with reStructuredText as understood by + Sphinx. Depending on the number of lines in the docstring, they are + layed out differently. If it's just one line, the closing tripple + quote is on the same line as the opening, otherwise the text is on + the same line as the opening quote and the tripple quote that closes + the string on its own line:: + + def foo(): + """This is a simple docstring""" + + + def bar(): + """This is a longer docstring with so much information in there + that it spans three lines. In this case the closing tripple quote + is on its own line. + """ + +Module header: + The module header consists of an utf-8 encoding declaration (if non + ASCII letters are used, but it is recommended all the time) and a + standard docstring:: + + # -*- coding: utf-8 -*- + """ + package.module + ~~~~~~~~~~~~~~ + + A brief description goes here. + + :copyright: (c) YEAR by AUTHOR. + :license: LICENSE_NAME, see LICENSE_FILE for more details. + """ + + Please keep in mind that proper copyrights and license files are a + requirement for approved Flask extensions. + + +Comments +-------- + +Rules for comments are similar to docstrings. Both are formatted with +reStructuredText. If a comment is used to document an attribute, put a +colon after the opening pound sign (``#``):: + + class User(object): + #: the name of the user as unicode string + name = Column(String) + #: the sha1 hash of the password + inline salt + pw_hash = Column(String) From 1202996c634b23f82421753cb7c46a5ccdf7aca2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 18:10:26 +0200 Subject: [PATCH 0381/3747] Fixed a template loading bug --- flask/templating.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index c55d4826..a6fd0982 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -35,23 +35,20 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): + loader = None try: module, name = template.split('/', 1) loader = self.app.modules[module].jinja_loader - if loader is None: - raise ValueError() except (ValueError, KeyError): - loader = self.app.jinja_loader - if loader is not None: - return loader.get_source(environment, template) - else: + pass + # if there was a module and it has a loader, try this first + if loader is not None: try: return loader.get_source(environment, name) except TemplateNotFound: pass - # raise the exception with the correct filename here. - # (the one that includes the prefix) - raise TemplateNotFound(template) + # fall back to application loader if module failed + return self.app.jinja_loader.get_source(environment, template) def list_templates(self): result = self.app.jinja_loader.list_templates() From c83b5555f05ebcff450908684bbf793ca561bea2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 18:11:38 +0200 Subject: [PATCH 0382/3747] Improved styleguide docs --- docs/styleguide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/styleguide.rst b/docs/styleguide.rst index bb99a79a..1387d4a6 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -71,7 +71,7 @@ Expressions and Statements -------------------------- General whitespace rules: - - Whitespace is absend for unary operators that are not works + - No whitespace for unary operators that are not words (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. - Whitespace is placed between binary operators. From 82b143f9720ea5837289af32785ce5ef951943d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 20:03:58 +0200 Subject: [PATCH 0383/3747] Changelog entry for 0.5.2 Conflicts: CHANGES --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index 9e95b349..3b77329f 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,14 @@ Release date to be announced, codename to be decided. is now optional to be consistent with the function of the same name on the application object. +Version 0.5.2 +------------- + +Bugfix Release, released on July 15th 2010 + +- fixed another issue with loading templates from directories when + modules were used. + Version 0.5.1 ------------- From dbb3620792eda43d5f778c7f9019609f62ad2bb9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 15 Jul 2010 21:22:11 +0200 Subject: [PATCH 0384/3747] Fixed an rst error --- docs/config.rst | 3 ++- docs/patterns/fileuploads.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index ab1923ba..e782bc7f 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -93,7 +93,8 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.5 ``SERVER_NAME`` -.. versionadded:: ``MAX_CONTENT_LENGTH`` +.. versionadded:: 0.6 + ``MAX_CONTENT_LENGTH`` Configuring from Files ---------------------- diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 884925cf..99f009c7 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -132,7 +132,7 @@ webserver's memory if the files are reasonable small otherwise in a temporary location (as returned by :func:`tempfile.gettempdir`). But how do you specify the maximum file size after which an upload is aborted? By default Flask will happily accept file uploads to an unlimited amount of -memory, but you can limit that by setting the `MAX_CONTENT_LENGTH` +memory, but you can limit that by setting the ``MAX_CONTENT_LENGTH`` config key:: from flask import Flask, Request From b4b2f42f48684fbb4c2d56f4f83d01b2be421a39 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 16 Jul 2010 01:18:20 +0800 Subject: [PATCH 0385/3747] Warn about SQL injection in the tutorial. --- docs/tutorial/views.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 0bce03a3..f2871257 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -48,6 +48,13 @@ redirect back to the `show_entries` page:: Note that we check that the user is logged in here (the `logged_in` key is present in the session and `True`). +.. admonition:: Security Note + + Be sure to use question marks when building SQL statements, as done in the + example above. Otherwise, your app will be vulnerable to SQL injection when + you use string formatting to build SQL statements. + See :ref:`sqlite3` for more. + Login and Logout ---------------- From 70dc2b66a0d02a23546ad8d48b7fca1c21997ae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 16 Jul 2010 13:14:54 +0200 Subject: [PATCH 0386/3747] Removed useless script reference. This fixes #87 --- docs/errorhandling.rst | 8 ++++---- examples/jqueryexample/templates/layout.html | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index ac9595f1..82d53b7c 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -71,9 +71,9 @@ We also tell the handler to only send errors and more critical messages. Because we certainly don't want to get a mail for warnings or other useless logs that might happen during request handling. -Before you run that in production, please also look at :ref:`log-format` -to put more information into that error mail. That will save you from a -lot of frustration. +Before you run that in production, please also look at :ref:`logformat` to +put more information into that error mail. That will save you from a lot +of frustration. Logging to a File @@ -110,7 +110,7 @@ above, just make sure to use a lower setting (I would recommend file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) -.. _log-format: +.. _logformat: Controlling the Log Format -------------------------- diff --git a/examples/jqueryexample/templates/layout.html b/examples/jqueryexample/templates/layout.html index 0b5f3a7e..3e2ed69b 100644 --- a/examples/jqueryexample/templates/layout.html +++ b/examples/jqueryexample/templates/layout.html @@ -2,8 +2,6 @@ jQuery Example - From 6fc1492357a94ab553fe7ef16a7a2235d05f5a1d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 11:36:16 +0200 Subject: [PATCH 0387/3747] Added make_response --- CHANGES | 2 ++ docs/api.rst | 2 ++ flask/__init__.py | 2 +- flask/helpers.py | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 18 ++++++++++++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3b77329f..24a6162e 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,8 @@ Release date to be announced, codename to be decided. - the endpoint for the :meth:`flask.Module.add_url_rule` method is now optional to be consistent with the function of the same name on the application object. +- added a :func:`flask.make_response` function that simplifies + creating response object instances in views. Version 0.5.2 ------------- diff --git a/docs/api.rst b/docs/api.rst index d7f887e4..f31563b4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -228,6 +228,8 @@ Useful Functions and Classes .. autofunction:: redirect +.. autofunction:: make_response + .. autofunction:: send_file .. autofunction:: send_from_directory diff --git a/flask/__init__.py b/flask/__init__.py index f4617271..93ada5f7 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -19,7 +19,7 @@ from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute + get_template_attribute, make_response from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string diff --git a/flask/helpers.py b/flask/helpers.py index fc309e67..6b57420c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -101,6 +101,48 @@ def jsonify(*args, **kwargs): indent=None if request.is_xhr else 2), mimetype='application/json') +def make_response(*args): + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html', 404)) + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index f2451223..ccf9b871 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -393,6 +393,24 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 400 assert rv.mimetype == 'text/plain' + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + assert rv.status_code == 200 + assert rv.data == '' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('Awesome') + assert rv.status_code == 200 + assert rv.data == 'Awesome' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('W00t', 404) + assert rv.status_code == 404 + assert rv.data == 'W00t' + assert rv.mimetype == 'text/html' + def test_url_generation(self): app = flask.Flask(__name__) @app.route('/hello/', methods=['POST']) From 90b8df3e4ce762ac4de4a6fabd8fb34532bedd5e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 13:10:50 +0200 Subject: [PATCH 0388/3747] Fixed paren in docstring --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 6b57420c..39214a10 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -124,7 +124,7 @@ def make_response(*args): view function. This for example creates a response with a 404 error code:: - response = make_response(render_template('not_found.html', 404)) + response = make_response(render_template('not_found.html'), 404) Internally this function does the following things: From a59dfe4a77ecfd8740cf55a719c88eedee07fb5c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 14:02:02 +0200 Subject: [PATCH 0389/3747] Added missing template --- tests/templates/nested/nested.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/templates/nested/nested.txt diff --git a/tests/templates/nested/nested.txt b/tests/templates/nested/nested.txt new file mode 100644 index 00000000..2c8634f9 --- /dev/null +++ b/tests/templates/nested/nested.txt @@ -0,0 +1 @@ +I'm nested From e0712b47c6e5bc98f2afd8b0a82cc5005dc58722 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 14:39:28 +0200 Subject: [PATCH 0390/3747] Added support for signals --- CHANGES | 5 ++ Makefile | 5 +- docs/api.rst | 51 ++++++++++++++++++ docs/conf.py | 3 +- flask/__init__.py | 4 ++ flask/app.py | 4 ++ flask/signals.py | 50 ++++++++++++++++++ flask/templating.py | 14 ++++- tests/flask_tests.py | 79 ++++++++++++++++++++++++++++ tests/templates/simple_template.html | 1 + 10 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 flask/signals.py create mode 100644 tests/templates/simple_template.html diff --git a/CHANGES b/CHANGES index 24a6162e..30ba418b 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,11 @@ Release date to be announced, codename to be decided. same name on the application object. - added a :func:`flask.make_response` function that simplifies creating response object instances in views. +- added signalling support based on blinker. This feature is currently + optional and supposed to be used by extensions and applications. If + you want to use it, make sure to have `blinker`_ installed. + +.. _blinker: http://pypi.python.org/pypi/blinker Version 0.5.2 ------------- diff --git a/Makefile b/Makefile index 4b1d8081..ba898b9e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test upload-docs +.PHONY: clean-pyc test upload-docs docs all: clean-pyc test @@ -20,3 +20,6 @@ upload-docs: scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf scp -r docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/ + +docs: + $(MAKE) -C docs html diff --git a/docs/api.rst b/docs/api.rst index f31563b4..afcb4c20 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -351,3 +351,54 @@ Useful Internals information from the context local around for a little longer. Make sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in that situation, otherwise your unittests will leak memory. + +Signals +------- + +.. versionadded:: 0.6 + +.. data:: signals_available + + `True` if the signalling system is available. This is the case + when `blinker`_ is installed. + +.. data:: template_rendered + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + +.. data:: request_started + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + +.. data:: request_finished + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + +.. data:: got_request_exception + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + +.. class:: flask.signals.Namespace + + An alias for :class:`blinker.base.Namespace` if blinker is available, + otherwise a dummy class that creates fake signals. This class is + available for Flask extensions that want to provide the same fallback + system as Flask itself. + + .. method:: signal(name, doc=None) + + Creates a new signal for this namespace if blinker is available, + otherwise returns a fake signal that has a send method that will + do nothing but will fail with a :exc:`RuntimeError` for all other + operations, including connecting. + +.. _blinker: http://pypi.python.org/pypi/blinker diff --git a/docs/conf.py b/docs/conf.py index 6308beee..721c8369 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -245,7 +245,8 @@ intersphinx_mapping = { 'http://docs.python.org/dev': None, 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, - 'http://wtforms.simplecodes.com/docs/0.5/': None + 'http://wtforms.simplecodes.com/docs/0.5/': None, + 'http://discorporate.us/projects/Blinker/docs/1.0/': None } pygments_style = 'flask_theme_support.FlaskyStyle' diff --git a/flask/__init__.py b/flask/__init__.py index 93ada5f7..95497069 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -24,6 +24,10 @@ from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string +# the signals +from .signals import signals_available, template_rendered, request_started, \ + request_finished, got_request_exception + # only import json if it's available if json_available: from .helpers import json diff --git a/flask/app.py b/flask/app.py index 16da7939..5860eb05 100644 --- a/flask/app.py +++ b/flask/app.py @@ -32,6 +32,7 @@ from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor +from .signals import request_started, request_finished, got_request_exception # a lock used for logger initialization _logger_lock = Lock() @@ -657,6 +658,7 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ + got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) if self.debug: raise @@ -791,6 +793,7 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: + request_started.send(self) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() @@ -801,6 +804,7 @@ class Flask(_PackageBoundObject): response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) + request_finished.send(self, response=response) return response(environ, start_response) def request_context(self, environ): diff --git a/flask/signals.py b/flask/signals.py new file mode 100644 index 00000000..a7e459af --- /dev/null +++ b/flask/signals.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" + flask.signals + ~~~~~~~~~~~~~ + + Implements signals based on blinker if available, otherwise + falls silently back to a noop + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +signals_available = False +try: + from blinker import Namespace + signals_available = True + _signals = Namespace() +except ImportError: + class Namespace(object): + def signal(self, name, doc=None): + return _FakeSignal(name, doc) + class _FakeSignal(object): + """If blinker is unavailable, create a fake class with the same + interface that allows sending of signals but will fail with an + error on anything else. Instead of doing anything on send, it + will just ignore the arguments and do nothing instead. + """ + + def __init__(self, name, doc=None): + self.name = name + self.__doc__ = doc + def _fail(self, *args, **kwargs): + raise RuntimeError('signalling support is unavailable ' + 'because the blinker library is ' + 'not installed.') + send = lambda *a, **kw: None + connect = disconnect = has_receivers_for = receivers_for = \ + temporarily_connected_to = _fail + del _fail + +# the namespace for code signals. If you are not flask code, do +# not put signals in here. Create your own namespace instead. +_signals = Namespace() + + +# core signals. For usage examples grep the sourcecode or consult +# the API documentation in docs/api.rst as well as docs/signals.rst +template_rendered = _signals.signal('template-rendered') +request_started = _signals.signal('request-started') +request_finished = _signals.signal('request-finished') +got_request_exception = _signals.signal('got-request-exception') diff --git a/flask/templating.py b/flask/templating.py index a6fd0982..41c0d39b 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -11,6 +11,7 @@ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from .globals import _request_ctx_stack +from .signals import template_rendered def _default_template_ctx_processor(): @@ -59,6 +60,13 @@ class _DispatchingJinjaLoader(BaseLoader): return result +def _render(template, context, app): + """Renders the template and fires the signal""" + rv = template.render(context) + template_rendered.send(app, template=template, context=context) + return rv + + def render_template(template_name, **context): """Renders a template from the template folder with the given context. @@ -69,7 +77,8 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.get_template(template_name).render(context) + return _render(ctx.app.jinja_env.get_template(template_name), + context, ctx.app) def render_template_string(source, **context): @@ -83,4 +92,5 @@ def render_template_string(source, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return ctx.app.jinja_env.from_string(source).render(context) + return _render(ctx.app.jinja_env.from_string(source), + context, ctx.app) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ccf9b871..43665e1c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1024,6 +1024,83 @@ class SubdomainTestCase(unittest.TestCase): assert rv.data == 'index for mitsuhiko' +class TestSignals(unittest.TestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + with flask.template_rendered.temporarily_connected_to(record, app): + rv = app.test_client().get('/') + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == 'simple_template.html' + assert context['whiskey'] == 42 + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + assert response.data == 'stuff' + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + assert rv.data == 'stuff' + + assert calls == ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal'] + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + with flask.got_request_exception.temporarily_connected_to(record): + assert app.test_client().get('/').status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -1038,6 +1115,8 @@ def suite(): suite.addTest(unittest.makeSuite(SubdomainTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) + if flask.signals_available: + suite.addTest(unittest.makeSuite(TestSignals)) suite.addTest(unittest.makeSuite(MiniTwitTestCase)) suite.addTest(unittest.makeSuite(FlaskrTestCase)) return suite diff --git a/tests/templates/simple_template.html b/tests/templates/simple_template.html new file mode 100644 index 00000000..c24612cb --- /dev/null +++ b/tests/templates/simple_template.html @@ -0,0 +1 @@ +

    {{ whiskey }}

    From c360f005c354f406735f4119d057a75b24e7baf1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:25:53 +0200 Subject: [PATCH 0391/3747] Added signal documentation --- docs/api.rst | 2 + docs/contents.rst.inc | 1 + docs/signals.rst | 205 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 docs/signals.rst diff --git a/docs/api.rst b/docs/api.rst index afcb4c20..ca57e8bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -355,6 +355,8 @@ Useful Internals Signals ------- +.. when modifying this list, also update the one in signals.rst + .. versionadded:: 0.6 .. data:: signals_available diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 27d0b73a..ba2de78f 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -15,6 +15,7 @@ instructions for web development with Flask. testing errorhandling config + signals shell patterns/index deploying/index diff --git a/docs/signals.rst b/docs/signals.rst new file mode 100644 index 00000000..7f2d4d97 --- /dev/null +++ b/docs/signals.rst @@ -0,0 +1,205 @@ +.. _signals: + +Signals +======= + +.. versionadded:: 0.6 + +Starting with Flask 0.6, there is integrated support for signalling in +Flask. This support is provided by the excellent `blinker`_ library and +will gracefully fall back if it is not available. + +What are signals? Signals help you decouple applications by sending +notifications when actions occur elsewhere in the core framework or +another Flask extensions. In short, signals allow certain senders to +notify subscribers that something happened. + +Flask comes with a couple of signals and other extensions might provide +more. Also keep in mind that signals are intended to notify subscribers +and should not encourage subscribers to modify data. You will notice that +there are signals that appear to do the same thing like some of the +builtin decorators do (eg: :data:`~flask.request_started` is very similar +to :meth:`~flask.Flask.before_request`). There are however difference in +how they work. The core :meth:`~flask.Flask.before_request` handler for +example is executed in a specific order and is able to abort the request +early by returning a response. In contrast all signal handlers are +executed in undefined order and do not modify any data. + +The big advantage of signals over handlers is that you can safely +subscribe to them for the split of a second. These temporary +subscriptions are helpful for unittesting for example. Say you want to +know what templates were rendered as part of a request: signals allow you +to do exactly that. + +Subscribing to Signals +---------------------- + +To subscribe to a signal, you can use the +:meth:`~blinker.base.Signal.connect` method of a signal. The first +argument is the function that should be called when the signal is emitted, +the optional second argument specifies a sender. To unsubscribe from a +signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. + +For all core Flask signals, the sender is the application that issued the +signal. This however might not be true for Flask extensions, so consult +the documentation when subscribing to signals. + +Additionally there is a convenient helper method that allows you to +temporarily subscribe a function to a signal. This is especially helpful +for unittests (:meth:`~blinker.base.Signal.temporarily_connected_to`). +This has to be used in combination with the `with` statement. + +Here for example a helper context manager that can be used to figure out +in a unittest which templates were rendered and what variables were passed +to the template:: + + from flask import template_rendered + from contextlib import contextmanager + + @contextmanager + def captured_templates(): + recorded = [] + def record(template, context): + recorded.append((template, context)) + template_rendered.connect(record) + try: + yield templates + finally: + template_rendered.disconnect(record) + +This can now easily be paired with a test client:: + + with captured_templates() as templates: + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert len(templates) == 1 + template, context = templates[0] + assert template.name == 'index.html' + assert len(context['items']) == 10 + +All the template rendering in the code, the `with` block wraps will now be +recorded in the `templates` variable. Whenever a template is rendered, +the template object as well as context is appended to it. + +Creating Signals +---------------- + +If you want to use signals in your own application, you can use the +blinker library directly. The most common use case are named signals in a +custom :class:`~blinker.base.Namespace`.. This is what is recommended +most of the time:: + + from blinker import Namespace + my_signals = Namespace() + +Now you can create new signals like this:: + + model_saved = my_signals.signal('model-saved') + +The name for the signal here makes it unique and also simplifies +debugging. You can access the name of the signal with the +:attr:`~blinker.base.NamedSignal.name` attribute. + +.. admonition:: For Extension Developers + + If you are writing a Flask extension and you to gracefully degrade for + missing blinker installations, you can do so by using the + :class:`flask.signals.Namespace` class. + +Sending Signals +--------------- + +If you want to emit a signal, you can do so by calling the +:meth:`~blinker.base.Signal.send` method. It accepts a sender as first +argument and optionally some keyword arguments that are forwarded to the +signal subscribers:: + + class Model(object): + ... + + def save(self): + model_saved.send(self) + +Try to always pick a good sender. If you have a class that is emitting a +signal, pass `self` as sender. If you emitting a signal from a random +function, you can pass ``current_app._get_current_object()`` as sender. + +.. admonition:: Passing Proxies as Senders + + Never pass :data:`~flask.current_app` as sender to a signal. Use + ``current_app._get_current_object()`` instead. The reason for this is + that :data:`~flask.current_app` is a proxy and not the real application + object. + +Core Signals +------------ + +.. when modifying this list, also update the one in api.rst + +The following signals exist in Flask: + +.. data:: flask.template_rendered + :noindex: + + This signal is sent when a template was successfully rendered. The + signal is invoked with the instance of the template as `template` + and the context as dictionary (named `context`). + + Example subscriber:: + + def log_template_renders(sender, template, context): + sender.logger.debug('Rendering template "%s" with context %s', + template.name or 'string template', + context) + + from flask import request_started + request_started.connect(log_template_renders) + +.. data:: flask.request_started + :noindex: + + This signal is sent before any request processing started but when the + request context was set up. Because the request context is already + bound, the subscriber can access the request with the standard global + proxies such as :class:`~flask.request`. + + Example subscriber:: + + def log_request(sender): + sender.logger.debug('Request context is set up') + + from flask import request_started + request_started.connect(log_request) + +.. data:: flask.request_finished + :noindex: + + This signal is sent right before the response is sent to the client. + It is passed the response to be sent named `response`. + + Example subscriber:: + + def log_response(sender, response): + sender.logger.debug('Request context is about to close down. ' + 'Response: %s', response) + + from flask import request_finished + request_finished.connect(log_response) + +.. data:: flask.got_request_exception + :noindex: + + This signal is sent when an exception happens during request processing. + It is sent *before* the standard exception handling kicks in and even + in debug mode, where no exception handling happens. The exception + itself is passed to the subscriber as `exception`. + + Example subscriber:: + + def log_exception(sender, exception): + sender.logger.debug('Got exception during processing: %s', exception) + + from flask import got_request_exception + got_request_exception.connect(log_exception) + +.. _blinker: http://pypi.python.org/pypi/blinker From 91e9632a379ebdd778c229a6da86d11140fa7f17 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:26:35 +0200 Subject: [PATCH 0392/3747] Moved wsgi_app down to a more logical location --- flask/app.py | 82 ++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/flask/app.py b/flask/app.py index 5860eb05..351c0c3a 100644 --- a/flask/app.py +++ b/flask/app.py @@ -766,47 +766,6 @@ class Flask(_PackageBoundObject): response = handler(response) return response - def wsgi_app(self, environ, start_response): - """The actual WSGI application. This is not implemented in - `__call__` so that middlewares can be applied without losing a - reference to the class. So instead of doing this:: - - app = MyMiddleware(app) - - It's a better idea to do this instead:: - - app.wsgi_app = MyMiddleware(app.wsgi_app) - - Then you still have the original application object around and - can continue to call methods on it. - - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - - :param environ: a WSGI environment - :param start_response: a callable accepting a status code, - a list of headers and an optional - exception context to start the response - """ - with self.request_context(environ): - try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - request_finished.send(self, response=response) - return response(environ, start_response) - def request_context(self, environ): """Creates a request context from the given environment and binds it to the current context. This must be used in combination with @@ -854,6 +813,47 @@ class Flask(_PackageBoundObject): from werkzeug import create_environ return self.request_context(create_environ(*args, **kwargs)) + def wsgi_app(self, environ, start_response): + """The actual WSGI application. This is not implemented in + `__call__` so that middlewares can be applied without losing a + reference to the class. So instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.4 + The :meth:`after_request` functions are now called even if an + error handler took over request processing. This ensures that + even if an exception happens database have the chance to + properly close the connection. + + :param environ: a WSGI environment + :param start_response: a callable accepting a status code, + a list of headers and an optional + exception context to start the response + """ + with self.request_context(environ): + try: + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + response = self.make_response(rv) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + try: + response = self.process_response(response) + except Exception, e: + response = self.make_response(self.handle_exception(e)) + request_finished.send(self, response=response) + return response(environ, start_response) + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) From a3a72e2d8d8df53164689fd84a5db2c6cd04f28a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:39:24 +0200 Subject: [PATCH 0393/3747] Added middlewares to quickstart. This fixes #88 --- docs/quickstart.rst | 11 +++++++++++ flask/app.py | 10 ++++++++++ flask/ctx.py | 3 +-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4e98f858..3604bdd1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -734,3 +734,14 @@ Here are some example log calls:: The attached :attr:`~flask.Flask.logger` is a standard logging :class:`~logging.Logger`, so head over to the official stdlib documentation for more information. + +Hooking in WSGI Middlewares +--------------------------- + +If you want to add a WSGI middleware to your application you can wrap the +internal WSGI application. For example if you want to one of the +middlewares from the Werkzeug package to work around bugs in lighttpd, you +can do it like this:: + + from werkzeug.contrib.fixers import LighttpdCGIRootFix + app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) diff --git a/flask/app.py b/flask/app.py index 351c0c3a..21235897 100644 --- a/flask/app.py +++ b/flask/app.py @@ -724,6 +724,16 @@ class Flask(_PackageBoundObject): return self.response_class(*rv) return self.response_class.force_type(rv, request.environ) + def create_url_adapter(self, request): + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set up + so the request is passed explicitly. + + .. versionadded:: 0.6 + """ + return self.url_map.bind_to_environ(request.environ, + server_name=self.config['SERVER_NAME']) + def preprocess_request(self): """Called before the actual request dispatching and will call every as :meth:`before_request` decorated function. diff --git a/flask/ctx.py b/flask/ctx.py index 854503af..1b17086c 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -28,9 +28,8 @@ class _RequestContext(object): def __init__(self, app, environ): self.app = app - self.url_adapter = app.url_map.bind_to_environ(environ, - server_name=app.config['SERVER_NAME']) self.request = app.request_class(environ) + self.url_adapter = app.create_url_adapter(self.request) self.session = app.open_session(self.request) if self.session is None: self.session = _NullSession() From e5008386b1a7e0ef3ebb5a4082bf2176d7169ab9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:41:05 +0200 Subject: [PATCH 0394/3747] Added docs on use_evalex. This fixes #90 --- flask/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/app.py b/flask/app.py index 21235897..0941cb7e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -371,6 +371,11 @@ class Flask(_PackageBoundObject): :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + .. admonition:: Keep in Mind Flask will suppress any server error with a generic error page From b6cae028f7a2c77108e5ef91e34c285a9ae7ad81 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:53:32 +0200 Subject: [PATCH 0395/3747] Removed useless instanciation --- flask/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/signals.py b/flask/signals.py index a7e459af..17de0447 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -13,7 +13,6 @@ signals_available = False try: from blinker import Namespace signals_available = True - _signals = Namespace() except ImportError: class Namespace(object): def signal(self, name, doc=None): From 405d4492e41b50f204d7e7d55aa0ac8f11edda1a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 15:56:58 +0200 Subject: [PATCH 0396/3747] Documented more changes --- CHANGES | 3 +++ flask/app.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 30ba418b..842c1a9c 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,9 @@ Release date to be announced, codename to be decided. - added signalling support based on blinker. This feature is currently optional and supposed to be used by extensions and applications. If you want to use it, make sure to have `blinker`_ installed. +- refactored the way url adapters are created. This process is now + fully customizable with the :meth:`~flask.Flask.create_url_adapter` + method. .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index 0941cb7e..c5f50135 100644 --- a/flask/app.py +++ b/flask/app.py @@ -311,7 +311,7 @@ class Flask(_PackageBoundObject): def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` - and :meth:`create_jinja_loader`. + and :meth:`select_jinja_autoescape`. .. versionadded:: 0.5 """ From 3ebc97a7809a15ae5f362d1330552db1efdec525 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:02:21 +0200 Subject: [PATCH 0397/3747] Fixed a typo in quickstart --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 3604bdd1..dc292e8f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -739,7 +739,7 @@ Hooking in WSGI Middlewares --------------------------- If you want to add a WSGI middleware to your application you can wrap the -internal WSGI application. For example if you want to one of the +internal WSGI application. For example if you want to use one of the middlewares from the Werkzeug package to work around bugs in lighttpd, you can do it like this:: From 97d2a198e216b29be5533dfba81525481bc9c1e5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:05:06 +0200 Subject: [PATCH 0398/3747] Added fallback for docs --- docs/conf.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 721c8369..932c9bcd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -250,3 +250,17 @@ intersphinx_mapping = { } pygments_style = 'flask_theme_support.FlaskyStyle' + +# fall back if theme is not there +try: + __import__('flask_theme_support') +except ImportError, e: + print '-' * 74 + print 'Warning: Flask themes unavailable. Building with default theme' + print 'If you want the Flask themes, run this command and build again:' + print + print ' git submodule init' + print '-' * 74 + + pygments_style = 'tango' + html_theme = 'default' From 5d011c356dfc659a4c2ef66e8c847d489aef4e9c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 16:12:21 +0200 Subject: [PATCH 0399/3747] Fixed fallback --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 932c9bcd..afe6bff3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -264,3 +264,4 @@ except ImportError, e: pygments_style = 'tango' html_theme = 'default' + html_theme_options = {} From 2b103134cf165ba453d5492e1a73b57b1e5bef1e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:09:01 +0200 Subject: [PATCH 0400/3747] Fixed command --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index afe6bff3..50eb8f90 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -259,7 +259,7 @@ except ImportError, e: print 'Warning: Flask themes unavailable. Building with default theme' print 'If you want the Flask themes, run this command and build again:' print - print ' git submodule init' + print ' git submodule update --init' print '-' * 74 pygments_style = 'tango' From 05519a85da1b26d73bc9215aa7f5cd357404f397 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:19:10 +0200 Subject: [PATCH 0401/3747] Fixing submodules --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..3d964b66 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 From 2a0e71f08140fe5c8d32ffed350ba403101f27cc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Jul 2010 17:22:10 +0200 Subject: [PATCH 0402/3747] Fixed a typo in an example in the docs --- docs/signals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index 7f2d4d97..eeed7343 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -63,7 +63,7 @@ to the template:: recorded.append((template, context)) template_rendered.connect(record) try: - yield templates + yield recorded finally: template_rendered.disconnect(record) From fd06bcfbf03efd496697083666953748ef3fe7a3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 00:51:20 +0200 Subject: [PATCH 0403/3747] Added notes on 3.x --- docs/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index 040412c6..a9dc5c28 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,6 +13,11 @@ So how do you get all that on your computer quickly? There are many ways which this section will explain, but the most kick-ass method is virtualenv, so let's look at that first. +Either way, you will need Python 2.5 or higher to get started, so be sure +to have an up to date Python 2.x installation. At the time of writing, +the WSGI specification is not yet finalized for Python 3, so Flask cannot +support the 3.x series of Python. + .. _virtualenv: virtualenv From 4a2d2ba3b8259165a97dc80890547f97d7e35ff5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 01:30:28 +0200 Subject: [PATCH 0404/3747] Added templating docs. This basically fixes #92 --- docs/contents.rst.inc | 1 + docs/quickstart.rst | 3 +- docs/security.rst | 9 ++ docs/templating.rst | 188 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 docs/templating.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index ba2de78f..f32d1da5 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -12,6 +12,7 @@ instructions for web development with Flask. installation quickstart tutorial/index + templating testing errorhandling config diff --git a/docs/quickstart.rst b/docs/quickstart.rst index dc292e8f..d7cf5982 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -379,7 +379,8 @@ package it's actually inside your package: /hello.html For templates you can use the full power of Jinja2 templates. Head over -to the `Jinja2 Template Documentation +to the :ref:`templating` section of the documentation or the official +`Jinja2 Template Documentation `_ for more information. Here an example template: diff --git a/docs/security.rst b/docs/security.rst index 05c9a62c..00f8bc2a 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -5,9 +5,18 @@ Web applications usually face all kinds of security problems and it's very hard to get everything right. Flask tries to solve a few of these things for you, but there are a couple more you have to take care of yourself. +.. _xss: + Cross-Site Scripting (XSS) -------------------------- +Cross site scripting is the concept of injecting arbitrary HTML (and with +it JavaScript) into the context of a website. To rememdy this, developers +have to properly escape text so that it cannot include arbitrary HTML +tags. For more information on that have a look at the Wikipedia article +on `Cross-Site Scripting +`_. + Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused in templates, but there are still other places where you have to be diff --git a/docs/templating.rst b/docs/templating.rst new file mode 100644 index 00000000..2583cc2c --- /dev/null +++ b/docs/templating.rst @@ -0,0 +1,188 @@ +Templates +========= + +Flask leverages Jinja2 as template engine. You are obviously free to use +a different template engine, but you still have to install Jinja2 to run +Flask itself. This requirement is necessary to enable rich extensions. +An extension can depend on Jinja2 being present. + +This section only gives a very quick introduction into how Jinja2 +is integrated into Flask. If you want information on the template +engine's syntax itself, head over to the official `Jinja2 Template +Documentation `_ for +more information. + +Jinja Setup +----------- + +Unless customized, Jinja2 is configured by Flask as follows: + +- autoescaping is enabled for all templates ending in ``.html``, + ``.htm``, ``.xml`` as well as ``.xhtml`` +- a template has the ability to opt in/out autoescaping with the + ``{% autoescape %}`` tag. +- Flask inserts a couple of global functions and helpers into the + Jinja2 context, additionally to the values that are present by + default. + +Standard Context +---------------- + +The following global variables are available within Jinja2 templates +by default: + +.. data:: config + :noindex: + + The current configuration object (:data:`flask.config`) + + .. versionadded:: 0.6 + +.. data:: request + :noindex: + + The current request object (:class:`flask.request`) + +.. data:: session + :noindex: + + The current session object (:class:`flask.session`) + +.. data:: g + :noindex: + + The request-bound object for global variables (:data:`flask.g`) + +.. function:: url_for + :noindex: + + The :func:`flask.url_for` function. + +.. function:: get_flashed_messages + :noindex: + + The :func:`flask.get_flashed_messages` function. + +.. admonition:: The Jinja Context Behaviour + + These variables are added to the context of variables, they are not + global variables. The difference is that by default these will not + show up in the context of imported templates. This is partially caused + by performance considerations, partially to keep things explicit. + + What does this mean for you? If you have a macro you want to import, + that needs to access the request object you have two possibilities: + + 1. you explicitly pass the request to the macro as parameter, or + the attribute of the request object you are interested in. + 2. you import the macro "with context". + + Importing with context looks like this: + + .. sourcecode:: jinja + + {% from '_helpers.html' import my_macro with context %} + +Standard Filters +---------------- + +These filters are available in Jinja2 additionally to the filters provided +by Jinja2 itself: + +.. function:: tojson + :noindex: + + This function converts the given object into JSON representation. This + is for example very helpful if you try to generate JavaScript on the + fly. + + Note that inside `script` tags no escaping must take place, so make + sure to disable escaping with ``|safe`` if you intend to use it inside + `script` tags: + + .. sourcecode:: html+jinja + + + + That the ``|tojson`` filter escapes forward slashes properly for you. + +Controlling Autoescaping +------------------------ + +Autoescaping is the concept of automatically escaping special characters +of you. Special characters in the sense of HTML (or XML, and thus XHTML) +are ``&``, ``>``, ``<``, ``"`` as well as ``'``. Because these characters +carry specific meanings in documents on their own you have to replace them +by so called "entities" if you want to use them for text. Not doing so +would not only cause user frustration by the inability to use these +characters in text, but can also lead to security problems. (see +:ref:`xss`) + +Sometimes however you will need to disable autoescaping in templates. +This can be the case if you want to explicitly inject HTML into pages, for +example if they come from a system that generate secure HTML like a +markdown to HTML converter. + +There are three ways to accomplish that: + +- In the Python code, wrap the HTML string in a :class:`~flask.Markup` + object before passing it to the template. This is in general the + recommended way. +- Inside the template, use the ``|safe`` filter to explicitly mark a + string as safe HTML (``{{ myvariable|safe }}``) +- Temporarily disable the autoescape system altogether. + +To disable the autoescape system in templates, you can use the ``{% +autoescape %}`` block: + +.. sourcecode:: html+jinja + + {% autoescape false %} +

    autoescaping is disabled here +

    {{ will_not_be_escaped }} + {% endautoescape %} + +Whenever you do this, please be very cautious about the varibles you are +using in this block. + +Registering Filters +------------------- + +If you want to register your own filters in Jinja2 you have two ways to do +that. You can either put them by hand into the +:attr:`~flask.Flask.jinja_env` of the application or use the +:meth:`~flask.Flask.template_filter` decorator. + +The two following examples work the same and both reverse an object:: + + @app.template_filter('reverse') + def reverse_filter(s): + return s[::-1] + + def reverse_filter(s): + return s[::-1] + app.jinja_env.filters['reverse'] = reverse_filter + +In case of the decorator the argument is optional if you want to use the +function name as name of the filter. + +Context Processors +------------------ + +To inject new variables automatically into the context of a template +context processors exist in Flask. Context processors run before the +template is rendered and have the ability to inject new values into the +template context. A context processor is a function that returns a +dictionary. The keys and values of this dictionary are then merged with +the template context:: + + @app.context_processor + def inject_user(): + return dict(user=g.user) + +The context processor above makes a variable called `user` available in +the template with the value of `g.user`. This example is not very +interesting because `g` is available in templates anyways, but it gives an +idea how this works. From 0d9b6347d6c77762fb888c067e91e428d65b8601 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 10:30:58 +0200 Subject: [PATCH 0405/3747] Fixed two typos in the pattern documentation. Thanks Ricky de Laveaga --- docs/patterns/jquery.rst | 2 +- docs/patterns/mongokit.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index a97f7ff4..f2ca39c5 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -40,7 +40,7 @@ Another method is using Google's `AJAX Libraries API In this case you don't have to put jQuery into your static folder, it will instead be loaded from Google directly. This has the advantage that your -website will probably load faster for users if they were to at least one +website will probably load faster for users if they went to at least one other website before using the same jQuery version from Google because it will already be in the browser cache. Downside is that if you don't have network connectivity during development jQuery will not load. diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index 27f182a3..c1727c80 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -7,7 +7,7 @@ Using a document database rather than a full DBMS gets more common these days. This pattern shows how to use MongoKit, a document mapper library, to integrate with MongoDB. -This pattern requires an running MongoDB server and the MongoKit library +This pattern requires a running MongoDB server and the MongoKit library installed. There are two very common ways to use MongoKit. I will outline each of them From 27d28dcef4cee8765dcd7a567689d35a41ddcd75 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:43:50 +0200 Subject: [PATCH 0406/3747] Added Session again to the public API because existing code might import it --- flask/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/__init__.py b/flask/__init__.py index 95497069..ee8508bc 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -23,6 +23,7 @@ from .helpers import url_for, jsonify, json_available, flash, \ from .globals import current_app, g, request, session, _request_ctx_stack from .module import Module from .templating import render_template, render_template_string +from .session import Session # the signals from .signals import signals_available, template_rendered, request_started, \ From 2912ff6f6e85983c372e1bc481337f3fdf6c7b9d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:44:07 +0200 Subject: [PATCH 0407/3747] Added some whitespace --- flask/signals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/signals.py b/flask/signals.py index 17de0447..22447c7c 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -17,6 +17,7 @@ except ImportError: class Namespace(object): def signal(self, name, doc=None): return _FakeSignal(name, doc) + class _FakeSignal(object): """If blinker is unavailable, create a fake class with the same interface that allows sending of signals but will fail with an From cd4833222eccf7e70380a42b69d5e766a84a6b63 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 20:57:02 +0200 Subject: [PATCH 0408/3747] Rewrote parts of the foreword and becoming big section --- docs/becomingbig.rst | 36 ++++++++++++++++++++++++------------ docs/foreword.rst | 38 ++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 916aa324..6c95c6e2 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -18,12 +18,13 @@ expand on that. Flask is designed to be extended and modified in a couple of different ways: +- Flask extensions. For a lot of reusable functionality you can create + extensions. For extensions a number of hooks exist throughout Flask + with signals and callback functions. + - Subclassing. The majority of functionality can be changed by creating a new subclass of the :class:`~flask.Flask` class and overriding - some methods. - -- Flask extensions. For a lot of reusable functionality you can create - extensions. + methods provided for this exact purpose. - Forking. If nothing else works out you can just take the Flask codebase at a given point and copy/paste it into your application @@ -49,8 +50,10 @@ reflected in the license of Flask. You don't have to contribute any changes back if you decide to modify the framework. The downside of forking is of course that Flask extensions will most -likely break because the new framework has a different import name and -because of that forking should be the last resort. +likely break because the new framework has a different import name. +Furthermore integrating upstream changes can be a complex process, +depending on the number of changes. Because of that, forking should be +the very last resort. Scaling like a Pro ------------------ @@ -68,9 +71,18 @@ support a second server. There is only one limiting factor regarding scaling in Flask which are the context local proxies. They depend on context which in Flask is -defined as being either a thread or a greenlet. Separate processes are -fine as well. If your server uses some kind of concurrency that is not -based on threads or greenlets, Flask will no longer be able to support -these global proxies. However the majority of servers are using either -threads, greenlets or separate processes to achieve concurrency which are -all methods well supported by the underlying Werkzeug library. +defined as being either a thread, process or greenlet. If your server +uses some kind of concurrency that is not based on threads or greenlets, +Flask will no longer be able to support these global proxies. However the +majority of servers are using either threads, greenlets or separate +processes to achieve concurrency which are all methods well supported by +the underlying Werkzeug library. + +Dialogue with the Community +--------------------------- + +The Flask developers are very interested to keep everybody happy, so as +soon as you find an obstacle in your way, caused by Flask, don't hesitate +to contact the developers on the mailinglist or IRC channel. The best way +for the Flask and Flask-extension developers to improve it for larger +applications is getting feedback from users. diff --git a/docs/foreword.rst b/docs/foreword.rst index b43fe870..58d47a3b 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -23,11 +23,22 @@ applications because changes on these thread-local objects can happen anywhere in the same thread. Flask provides some tools to deal with the downsides of this approach but -it might be an issue for larger applications. Flask is also based on -convention over configuration, which means that many things are -preconfigured and will work well for smaller applications but not so well -for larger ones. For example, by convention, templates and static files -are in subdirectories within the Python source tree of the application. +it might be an issue for larger applications because in theory +modifications on these objects might happen anywhere in the same thread. + +Flask is also based on convention over configuration, which means that +many things are preconfigured. For example, by convention, templates and +static files are in subdirectories within the Python source tree of the +application. + +The main reason however why Flask is called a "microframework" is the idea +to keep the core simple but extensible. There is database abstraction +layer, no form validation or anything else where different libraries +already exist that can handle that. However Flask knows the concept of +extensions that can add this functionality into your application as if it +was implemented in Flask itself. There are currently extensions for +object relational mappers, form validation, upload handling, various open +authentication technologies and more. However Flask is not much code and built in a very solid foundation and with that very easy to adapt for large applications. If you are @@ -69,20 +80,3 @@ probing for ways to fill your database with spam, links to malicious software, and the like. So always keep security in mind when doing web development. - -Target Audience ---------------- - -Is Flask for you? If your application is small or medium sized and does -not depend on very complex database structures, Flask is the Framework for -you. It was designed from the ground up to be easy to use, and built on -the firm foundation of established principles, good intentions, and -mature, widely used libraries. Recent versions of Flask scale nicely -within reasonable bounds, and if you grow larger, you won't have any -trouble adjusting Flask for your new application size. - -If you suddenly discover that your application grows larger than -originally intended, head over to the :ref:`becomingbig` section to see -some possible solutions for larger applications. - -Satisfied? Then let's proceed with :ref:`installation`. From 9991cfa96b8babe1c59bb4b3548df76e01af07f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:02:56 +0200 Subject: [PATCH 0409/3747] Added notes for Python 3 --- docs/foreword.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/foreword.rst b/docs/foreword.rst index 58d47a3b..c808307f 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -80,3 +80,27 @@ probing for ways to fill your database with spam, links to malicious software, and the like. So always keep security in mind when doing web development. + +The Status of Python 3 +---------------------- + +Currently the Python community is in the process of improving libraries to +support the new iteration of the Python programming language. +Unfortunately there are a few problems with Python 3, namely the missing +consent on what WSGI for Python 3 should look like. These problems are +partially caused by changes in the language that went unreviewed for too +long, also partially the ambitions of everyone involved to drive the WSGI +standard forward. + +Because of that we strongly recommend against using Python 3 for web +development of any kind and wait until the WSGI situation is resolved. +You will find a couple of frameworks and web libraries on PyPI that claim +Python 3 support, but this support is based on the broken WSGI +implementation provided by Python 3.0 and 3.1 which will most likely +change in the near future. + +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +WSGI is found, and we will provide helpful tips how to upgrade existing +applications to Python 3. Until then, we strongly recommend using Python +2.6 and 2.7 with activated Python 3 warnings during development, as well + as the unicode literals `__future__` feature. From 8837678a088ad4ec710d78de3c30f87be2d13de6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:06:46 +0200 Subject: [PATCH 0410/3747] Changed extensiondev docs --- docs/extensiondev.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 542ec192..3319d229 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -264,9 +264,17 @@ development. If you want to learn more, it's a very good idea to check out existing extensions on the `Flask Extension Registry`_. If you feel lost there is still the `mailinglist`_ and the `IRC channel`_ to get some ideas for nice looking APIs. Especially if you do something nobody before -you did, it might be a very good idea to get some more input. +you did, it might be a very good idea to get some more input. This not +only to get an idea about what people might want to have from an +extension, but also to avoid having multiple developers working on pretty +much the same side by side. -Remember: good API design is hard :( +Remember: good API design is hard, so introduce your project on the +mailinglist, and let other developers give you a helping hand with +designing the API. + +The best Flask extensions are extensions that share common idioms for the +API. And this can only work if collaboration happens early. .. _Flask Extension Wizard: From e269c8eb727bdf72a0ca22e42b895dd37f3670c8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:07:45 +0200 Subject: [PATCH 0411/3747] Removed unused LICENSE file. The doc license is covered by the project's LICENSE file --- docs/LICENSE | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 docs/LICENSE diff --git a/docs/LICENSE b/docs/LICENSE deleted file mode 100644 index af19e1a7..00000000 --- a/docs/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS -for more details. - -Some rights reserved. - -Redistribution and use in source (reStructuredText) and 'compiled' forms (HTML, -PDF, PostScript, RTF and so forth) with or without modification, are permitted -provided that the following conditions are met: - -* Redistributions of source code (reStructuredText) must retain the above - copyright notice, this list of conditions and the following disclaimer as the - first lines of this file unmodified. - -* Redistributions in compiled form (converted to HTML, PDF, PostScript, RTF - and so forth) must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other - materials provided with the distribution. - -THIS DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From cb660cd1f1a30f67d8ea2b305d635ce573d239c6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:08:58 +0200 Subject: [PATCH 0412/3747] Fixed a typo in the html faq --- docs/htmlfaq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst index ca90ae06..1da25f3d 100644 --- a/docs/htmlfaq.rst +++ b/docs/htmlfaq.rst @@ -130,7 +130,7 @@ supported by browsers: - Wrapping the document in an ```` tag - Wrapping header elements in ```` or the body elements in ```` -- Closing the ``

    ``, ``

  • ``, ``
    ``, ``
    ``, ````, +- Closing the ``

    ``, ``

  • ``, ``
    ``, ``
    ``, ````, ````, ````, ````, ````, or ```` tags. - Quoting attributes, so long as they contain no whitespace or special characters (like ``<``, ``>``, ``'``, or ``"``). From 559d2810d7b6f2938cac402636cc689016f28520 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 21:58:18 +0200 Subject: [PATCH 0413/3747] Expanded the security docs to mention unquoted attributes as dangerous --- docs/foreword.rst | 2 +- docs/security.rst | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index c808307f..e4c72220 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -103,4 +103,4 @@ Werkzeug and Flask will be ported to Python 3 as soon as a solution for WSGI is found, and we will provide helpful tips how to upgrade existing applications to Python 3. Until then, we strongly recommend using Python 2.6 and 2.7 with activated Python 3 warnings during development, as well - as the unicode literals `__future__` feature. +as the unicode literals `__future__` feature. diff --git a/docs/security.rst b/docs/security.rst index 00f8bc2a..45fff0ca 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -30,6 +30,31 @@ careful: content-type guessing based on the first few bytes so users could trick a browser to execute HTML. +Another thing that is very important are unquoted attributes. While +Jinja2 can protect you from XSS issues by escaping HTML, there is one +thing it cannot protect you from: XSS by attribute injection. To counter +this possible attack vector, be sure to always quote your attributes with +either double or single quotes when using Jinja expressions in them: + +.. sourcecode:: html+jinja + + the text + +Why is this necessary? Because if you would not be doing that, an +attacker could easily inject custom JavaScript handlers. For example an +attacker could inject this piece of HTML+JavaScript: + +.. sourcecode:: html + + onmouseover=alert(document.cookie) + +When the user would then move with the mouse over the link, the cookie +would be presented to the user in an alert window. But instead of showing +the cookie to the user, a good attacker might also execute any other +JavaScript code. In combination with CSS injections the attacker might +even make the element fill out the entire page so that the user would +just have to have the mouse anywhere on the page to trigger the attack. + Cross-Site Request Forgery (CSRF) --------------------------------- From c0abdc4fa5bd63d38974a0c8f28d4b3ba6ca5eb3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Jul 2010 22:01:11 +0200 Subject: [PATCH 0414/3747] Interlinked design docs better --- docs/design.rst | 6 ++++++ docs/foreword.rst | 3 +++ 2 files changed, 9 insertions(+) diff --git a/docs/design.rst b/docs/design.rst index 20c57a1f..1f391b8c 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -1,3 +1,5 @@ +.. _design: + Design Decisions in Flask ========================= @@ -109,6 +111,10 @@ A template abstraction layer that would not take the unique features of the template engines away is a science on its own and a too large undertaking for a microframework like Flask. +Furthermore extensions can then easily depend on one template language +being present. You can easily use your own templating language, but an +extension could still depend on Jinja itself. + Micro with Dependencies ----------------------- diff --git a/docs/foreword.rst b/docs/foreword.rst index e4c72220..3a7521d4 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -44,6 +44,9 @@ However Flask is not much code and built in a very solid foundation and with that very easy to adapt for large applications. If you are interested in that, check out the :ref:`becomingbig` chapter. +If you are curious about the Flask design principles, head over to the +section about :ref:`design`. + A Framework and an Example -------------------------- From 3b0eb0f3ca45786135cacfc380bfdb51e7744e46 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 10:12:58 +0100 Subject: [PATCH 0415/3747] Added notes on proxies --- docs/api.rst | 35 +++++++++++++++++++++++++++++++++++ docs/signals.rst | 46 ++++++++++++++++++++++++++++------------------ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ca57e8bf..ed2a5037 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -38,6 +38,8 @@ Incoming Request Data sure that you always get the correct data for the active thread if you are in a multithreaded environment. + This is a proxy. See :ref:`notes-on-proxies` for more information. + The request object is an instance of a :class:`~werkzeug.Request` subclass and provides all of the attributes Werkzeug defines. This just shows a quick overview of the most important ones. @@ -164,6 +166,8 @@ To access the current session you can use the :class:`session` object: The session object works pretty much like an ordinary dict, with the difference that it keeps track on modifications. + This is a proxy. See :ref:`notes-on-proxies` for more information. + The following attributes are interesting: .. attribute:: new @@ -206,6 +210,8 @@ thing, like it does for :class:`request` and :class:`session`. Just store on this whatever you want. For example a database connection or the user that is currently logged in. + This is a proxy. See :ref:`notes-on-proxies` for more information. + Useful Functions and Classes ---------------------------- @@ -216,6 +222,8 @@ Useful Functions and Classes extensions that want to support multiple applications running side by side. + This is a proxy. See :ref:`notes-on-proxies` for more information. + .. autofunction:: url_for .. function:: abort(code) @@ -389,6 +397,8 @@ Signals in debug mode, where no exception handling happens. The exception itself is passed to the subscriber as `exception`. +.. currentmodule:: None + .. class:: flask.signals.Namespace An alias for :class:`blinker.base.Namespace` if blinker is available, @@ -404,3 +414,28 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +reason behind this is, that these proxies are shared between threads and +they have to dispatch to the actual object bound to a thread behind the +scenes as necessary. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is an actual proxy: + +- The proxy objects do not fake their inherited types, so if you want to + perform actual instance checks, you have to do that on the instance + that +- if the object reference is important (so for example for sending + :ref:`signals`) + +If you need to get access to the underlying object that is proxied, you +can use the :meth:`~werkzeug.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) diff --git a/docs/signals.rst b/docs/signals.rst index eeed7343..513d8694 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -41,13 +41,9 @@ the optional second argument specifies a sender. To unsubscribe from a signal, you can use the :meth:`~blinker.base.Signal.disconnect` method. For all core Flask signals, the sender is the application that issued the -signal. This however might not be true for Flask extensions, so consult -the documentation when subscribing to signals. - -Additionally there is a convenient helper method that allows you to -temporarily subscribe a function to a signal. This is especially helpful -for unittests (:meth:`~blinker.base.Signal.temporarily_connected_to`). -This has to be used in combination with the `with` statement. +signal. When you subscribe to a signal, be sure to also provide a sender +unless you really want to listen for signals of all applications. This is +especially true if you are developing an extension. Here for example a helper context manager that can be used to figure out in a unittest which templates were rendered and what variables were passed @@ -57,19 +53,19 @@ to the template:: from contextlib import contextmanager @contextmanager - def captured_templates(): + def captured_templates(app): recorded = [] def record(template, context): recorded.append((template, context)) - template_rendered.connect(record) + template_rendered.connect(record, app) try: yield recorded finally: - template_rendered.disconnect(record) + template_rendered.disconnect(record, app) This can now easily be paired with a test client:: - with captured_templates() as templates: + with captured_templates(app) as templates: rv = app.test_client().get('/') assert rv.status_code == 200 assert len(templates) == 1 @@ -77,9 +73,23 @@ This can now easily be paired with a test client:: assert template.name == 'index.html' assert len(context['items']) == 10 -All the template rendering in the code, the `with` block wraps will now be -recorded in the `templates` variable. Whenever a template is rendered, -the template object as well as context is appended to it. +All the template rendering in the code issued by the application `app` +in the body of the `with` block will now be recorded in the `templates` +variable. Whenever a template is rendered, the template object as well as +context are appended to it. + +Additionally there is a convenient helper method +(:meth:`~blinker.base.Signal.temporarily_connected_to`). that allows you +to temporarily subscribe a function to a signal with is a context manager +on its own which simplifies the example above:: + + from flask import template_rendered + + def captured_templates(app): + recorded = [] + def record(template, context): + recorded.append((template, context)) + return template_rendered.temporarily_connected_to(record, app) Creating Signals ---------------- @@ -153,7 +163,7 @@ The following signals exist in Flask: context) from flask import request_started - request_started.connect(log_template_renders) + request_started.connect(log_template_renders, app) .. data:: flask.request_started :noindex: @@ -169,7 +179,7 @@ The following signals exist in Flask: sender.logger.debug('Request context is set up') from flask import request_started - request_started.connect(log_request) + request_started.connect(log_request, app) .. data:: flask.request_finished :noindex: @@ -184,7 +194,7 @@ The following signals exist in Flask: 'Response: %s', response) from flask import request_finished - request_finished.connect(log_response) + request_finished.connect(log_response, app) .. data:: flask.got_request_exception :noindex: @@ -200,6 +210,6 @@ The following signals exist in Flask: sender.logger.debug('Got exception during processing: %s', exception) from flask import got_request_exception - got_request_exception.connect(log_exception) + got_request_exception.connect(log_exception, app) .. _blinker: http://pypi.python.org/pypi/blinker From c5b1755317c7bdfc20f4395a9a29a83b5b9cfd43 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 13:48:13 +0100 Subject: [PATCH 0416/3747] Added testcase for modified URL encodings --- tests/flask_tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 43665e1c..698094b9 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -496,6 +496,21 @@ class JSONTestCase(unittest.TestCase): rv = render('{{ "<\0/script>"|tojson|safe }}') assert rv == '"<\\u0000\\/script>"' + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + assert rv.status_code == 200 + assert rv.data == u'정상처리'.encode('utf-8') + class TemplatingTestCase(unittest.TestCase): From 05108673685c709c9929ab0e76451b74d51fb79b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:05:01 +0100 Subject: [PATCH 0417/3747] Documented blinker 1.1 changes --- docs/signals.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 513d8694..feb9a7b2 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -79,9 +79,9 @@ variable. Whenever a template is rendered, the template object as well as context are appended to it. Additionally there is a convenient helper method -(:meth:`~blinker.base.Signal.temporarily_connected_to`). that allows you -to temporarily subscribe a function to a signal with is a context manager -on its own which simplifies the example above:: +(:meth:`~blinker.base.Signal.connected_to`). that allows you to +temporarily subscribe a function to a signal with is a context manager on +its own which simplifies the example above:: from flask import template_rendered @@ -89,7 +89,12 @@ on its own which simplifies the example above:: recorded = [] def record(template, context): recorded.append((template, context)) - return template_rendered.temporarily_connected_to(record, app) + return template_rendered.connected_to(record, app) + +.. admonition:: Blinker API Changes + + The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker + with version 1.1. Creating Signals ---------------- @@ -141,6 +146,18 @@ function, you can pass ``current_app._get_current_object()`` as sender. that :data:`~flask.current_app` is a proxy and not the real application object. +Decorator Based Signal Subscriptions +------------------------------------ + +With Blinker 1.1 you can also easily subscribe to signals by using the new +:meth:`~blinker.base.NamedSignal.connect_via` decorator:: + + from flask import template_rendered + + @template_rendered.connect_via(app) + def when_template_rendered(template, context): + print 'Template %s is rendered with %s' % (template.name, context) + Core Signals ------------ From 80268a408b981254bbf2bd318aaade9a21a281ef Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:05:10 +0100 Subject: [PATCH 0418/3747] Documented proxy fix --- docs/conf.py | 1 + docs/deploying/others.rst | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 50eb8f90..601edc0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,6 +246,7 @@ intersphinx_mapping = { 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None, + # TODO: push to 1.1 'http://discorporate.us/projects/Blinker/docs/1.0/': None } diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 1ec94be4..793a4bed 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -61,3 +61,38 @@ and `greenlet`_. Running a Flask application on this server is quite simple:: .. _eventlet: http://eventlet.net/ .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html + +Proxy Setups +------------ + +If you deploy your application behind an HTTP proxy you will need to +rewrite a few headers in order for the application to work. The two +problematic values in the WSGI environment usually are `REMOTE_ADDR` and +`HTTP_HOST`. Werkzeug ships a fixer that will solve some common setups, +but you might want to write your own WSGI middlware for specific setups. + +The most common setup invokes the host being set from `X-Forwarded-Host` +and the remote address from `X-Forwared-For`:: + + from werkzeug.contrib.fixers import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app) + +Please keep in mind that it is a security issue to use such a middleware +in a non-proxy setup because it will blindly trust the incoming +headers which might be forged by malicious clients. + +If you want to rewrite the headers from another header, you might want to +use a fixer like this:: + + class CustomProxyFix(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + host = environ.get('HTTP_X_FHOST', '') + if host: + environ['HTTP_HOST'] = host + return self.app(environ, start_response) + + app.wsgi_app = CustomProxyFix(app.wsgi_app) From b49afa21ade898061d279ea20ba964d7e583c419 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Jul 2010 15:09:51 +0100 Subject: [PATCH 0419/3747] Removed temp subscription contextmanager in blinker tests to support upcoming api improvements better --- tests/flask_tests.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 698094b9..5d5c3794 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1052,12 +1052,15 @@ class TestSignals(unittest.TestCase): def record(sender, template, context): recorded.append((template, context)) - with flask.template_rendered.temporarily_connected_to(record, app): + flask.template_rendered.connect(record, app) + try: rv = app.test_client().get('/') assert len(recorded) == 1 template, context = recorded[0] assert template.name == 'simple_template.html' assert context['whiskey'] == 42 + finally: + flask.template_rendered.disconnect(record, app) def test_request_signals(self): app = flask.Flask(__name__) @@ -1110,10 +1113,13 @@ class TestSignals(unittest.TestCase): def record(sender, exception): recorded.append(exception) - with flask.got_request_exception.temporarily_connected_to(record): + flask.got_request_exception.connect(record, app) + try: assert app.test_client().get('/').status_code == 500 assert len(recorded) == 1 assert isinstance(recorded[0], ZeroDivisionError) + finally: + flask.got_request_exception.disconnect(record, app) def suite(): From 7680d52f42a562eda2e3f75b34118abb08d93414 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 13:29:21 +0100 Subject: [PATCH 0420/3747] Added support for subdomain bound modules --- CHANGES | 3 +++ flask/app.py | 1 + flask/module.py | 12 ++++++++++-- tests/flask_tests.py | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 842c1a9c..c1f50998 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,9 @@ Release date to be announced, codename to be decided. - refactored the way url adapters are created. This process is now fully customizable with the :meth:`~flask.Flask.create_url_adapter` method. +- modules can now register for a subdomain instead of just an URL + prefix. This makes it possible to bind a whole module to a + configurable subdomain. .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index c5f50135..151525e6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -454,6 +454,7 @@ class Flask(_PackageBoundObject): provided. """ options.setdefault('url_prefix', module.url_prefix) + options.setdefault('subdomain', module.subdomain) state = _ModuleSetupState(self, **options) for func in module._register_events: func(state) diff --git a/flask/module.py b/flask/module.py index 9eaa4f82..e6e1ee36 100644 --- a/flask/module.py +++ b/flask/module.py @@ -37,9 +37,10 @@ def _register_module(module, static_path): class _ModuleSetupState(object): - def __init__(self, app, url_prefix=None): + def __init__(self, app, url_prefix=None, subdomain=None): self.app = app self.url_prefix = url_prefix + self.subdomain = subdomain class Module(_PackageBoundObject): @@ -94,6 +95,9 @@ class Module(_PackageBoundObject): modules to refer to their own templates and static files. See :ref:`modules-and-resources` for more information. + .. versionadded:: 0.6 + The `subdomain` parameter was added. + :param import_name: the name of the Python package or module implementing this :class:`Module`. :param name: the internal short name for the module. Unless specified @@ -101,6 +105,8 @@ class Module(_PackageBoundObject): :param url_prefix: an optional string that is used to prefix all the URL rules of this module. This can also be specified when registering the module with the application. + :param subdomain: used to set the subdomain setting for URL rules that + do not have a subdomain setting set. :param static_path: can be used to specify a different path for the static files on the web. Defaults to ``/static``. This does not affect the folder the files are served @@ -108,7 +114,7 @@ class Module(_PackageBoundObject): """ def __init__(self, import_name, name=None, url_prefix=None, - static_path=None): + static_path=None, subdomain=None): if name is None: assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' @@ -116,6 +122,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix + self.subdomain = subdomain self._register_events = [_register_module(self, static_path)] def route(self, rule, **options): @@ -140,6 +147,7 @@ class Module(_PackageBoundObject): the_rule = rule if state.url_prefix: the_rule = state.url_prefix + rule + options.setdefault('subdomain', state.subdomain) the_endpoint = endpoint if the_endpoint is None: the_endpoint = _endpoint_from_view_func(view_func) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5d5c3794..3000e41c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1038,6 +1038,27 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://mitsuhiko.localhost/') assert rv.data == 'index for mitsuhiko' + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + assert rv.data == 'Test' + rv = c.get('/outside', 'http://xtesting.localhost/') + assert rv.data == 'Outside' + class TestSignals(unittest.TestCase): From dcf5dff414f2f4b9b24047510d3fa7ac643e971f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 14:58:51 +0100 Subject: [PATCH 0421/3747] Updatd an intersphinx link --- docs/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 601edc0a..3be35fb6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,8 +246,7 @@ intersphinx_mapping = { 'http://werkzeug.pocoo.org/documentation/dev/': None, 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None, - # TODO: push to 1.1 - 'http://discorporate.us/projects/Blinker/docs/1.0/': None + 'http://discorporate.us/projects/Blinker/docs/1.1/': None } pygments_style = 'flask_theme_support.FlaskyStyle' From 3adc9de5ec39e65f64bcd684613c64b29131a64e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 14:59:10 +0100 Subject: [PATCH 0422/3747] Updated docs for extension approval process --- docs/extensiondev.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 3319d229..70cfbeb7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -277,6 +277,46 @@ The best Flask extensions are extensions that share common idioms for the API. And this can only work if collaboration happens early. +Approved Extensions +------------------- + +Flask also has the concept of approved extensions. Approved extensions +are tested as part of Flask itself to ensure extensions do not break on +new releases. These approved extensions are listed on the `Flask +Extension Registry`_ and marked appropriately. If you want your own +extension to be approved you have to follow these guidelines: + +1. An approved Flask extension must provide exactly one package or module + inside the `flaskext` namespace package. +2. It must ship a testsuite that can either be invoked with ``make test`` + or ``python setup.py test``. For testsuites invoked with ``make + test`` the extension has to ensure that all dependencies for the test + are installed automatically, in case of ``python setup.py test`` + dependencies for tests alone can be specified in the `setup.py` + file. The testsuite also has to be part of the distribution. +3. APIs of approved extensions will be checked for the following + behavioristics: + + - an approved extension has to support multiple applications + running in the same Python process. + - it must be possible to use the factory pattern for creating + applications. + +4. The license has to be BSD/MIT/WTFPL licensed unless a depending + library absolutely enforces GPL or another license. +5. The naming scheme for official extensions is *Flask-ExtensionName* or + *ExtensionName-Flask*. +6. Approved extensions must define all their dependencies in the + `setup.py` file unless a dependency cannot by met because it is not + available on PyPI. +7. The extension must have documentation that furthermore uses one of + the two Flask themes for Sphinx documentation. +8. The setup.py description (and thus the PyPI description) has to + link to the documentation, website (if there is one) and there + must be a link to automatically install the development version + (``PackageName==dev``). + + .. _Flask Extension Wizard: http://github.com/mitsuhiko/flask-extension-wizard .. _OAuth extension: http://packages.python.org/Flask-OAuth/ From 66c13955895c7afb849513d9d0ff0f7f8361c511 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:21:08 +0100 Subject: [PATCH 0423/3747] Fixed wording --- docs/extensiondev.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 70cfbeb7..a22b5855 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -309,8 +309,8 @@ extension to be approved you have to follow these guidelines: 6. Approved extensions must define all their dependencies in the `setup.py` file unless a dependency cannot by met because it is not available on PyPI. -7. The extension must have documentation that furthermore uses one of - the two Flask themes for Sphinx documentation. +7. The extension must have documentation that uses one of the two Flask + themes for Sphinx documentation. 8. The setup.py description (and thus the PyPI description) has to link to the documentation, website (if there is one) and there must be a link to automatically install the development version From f4bfae622b5515ebde795fb71e3cc128cc0d5ead Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:41:39 +0100 Subject: [PATCH 0424/3747] Added flaskext tester --- tests/flaskext_test.py | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/flaskext_test.py diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py new file mode 100644 index 00000000..aa4b7461 --- /dev/null +++ b/tests/flaskext_test.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +""" + Flask Extension Tests + ~~~~~~~~~~~~~~~~~~~~~ + + Tests the Flask extensions. + + :copyright: (c) 2010 by Ali Afshar. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import tempfile, subprocess, urllib2, os + +from flask import json + +from setuptools.package_index import PackageIndex +from setuptools.archive_util import unpack_archive + +flask_svc_url = 'http://flask.pocoo.org/extensions/' +tdir = tempfile.mkdtemp() + + +def run_tests(checkout_dir): + cmd = ['tox'] + return subprocess.call(cmd, cwd=checkout_dir, + stdout=open(os.path.join(tdir, 'tox.log'), 'w'), + stderr=subprocess.STDOUT) + + +def get_test_command(checkout_dir): + files = set(os.listdir(checkout_dir)) + if 'Makefile' in files: + return 'make test' + elif 'conftest.py' in files: + return 'py.test' + else: + return 'nosetests' + + +def fetch_extensions_list(): + req = urllib2.Request(flask_svc_url, headers={'accept':'application/json'}) + d = urllib2.urlopen(req).read() + data = json.loads(d) + for ext in data['extensions']: + yield ext + + +def checkout_extension(ext): + name = ext['name'] + root = os.path.join(tdir, name) + os.mkdir(root) + checkout_path = PackageIndex().download(ext['name'], root) + unpack_archive(checkout_path, root) + path = None + for fn in os.listdir(root): + path = os.path.join(root, fn) + if os.path.isdir(path): + break + return path + + +tox_template = """[tox] +envlist=py26 + +[testenv] +commands= +%s +downloadcache= +%s +""" + +def create_tox_ini(checkout_path): + tox_path = os.path.join(checkout_path, 'tox.ini') + if not os.path.exists(tox_path): + with open(tox_path, 'w') as f: + f.write(tox_template % (get_test_command(checkout_path), tdir)) + +# XXX command line +only_approved = True + +def test_all_extensions(only_approved=only_approved): + for ext in fetch_extensions_list(): + if ext['approved'] or not only_approved: + checkout_path = checkout_extension(ext) + create_tox_ini(checkout_path) + ret = run_tests(checkout_path) + yield ext['name'], ret + +def main(): + for name, ret in test_all_extensions(): + print name, ret + + +if __name__ == '__main__': + main() From cb79b58053c34dc222c84584da506650ec64ee6f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:41:58 +0100 Subject: [PATCH 0425/3747] Added ali to authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index c2415977..936b0b0f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Patches and Suggestions ``````````````````````` - Adam Zapletal +- Ali Afshar - Chris Edgemon - Chris Grindstaff - Christopher Grebs From b2fb2245f1a115c65cba42c6c34f0eae30da4c6c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 15:42:33 +0100 Subject: [PATCH 0426/3747] Fixed a word in the docs --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index a22b5855..9eb2dbbe 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -295,7 +295,7 @@ extension to be approved you have to follow these guidelines: dependencies for tests alone can be specified in the `setup.py` file. The testsuite also has to be part of the distribution. 3. APIs of approved extensions will be checked for the following - behavioristics: + characteristics: - an approved extension has to support multiple applications running in the same Python process. From 63114529ed51213832c9ada0b6216ec9e8e22052 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Jul 2010 17:49:03 +0100 Subject: [PATCH 0427/3747] good -> bad --- docs/tutorial/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index ed9fc3e3..34edd791 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -4,7 +4,7 @@ Bonus: Testing the Application ============================== Now that you have finished the application and everything works as -expected, it's probably not a good idea to add automated tests to simplify +expected, it's probably not a bad idea to add automated tests to simplify modifications in the future. The application above is used as a basic example of how to perform unittesting in the :ref:`testing` section of the documentation. Go there to see how easy it is to test Flask applications. From a7d83a9f8008a58813e4495dd8b00d14214a51ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 14:59:31 +0200 Subject: [PATCH 0428/3747] Approved extensions must not be GPL --- docs/extensiondev.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 9eb2dbbe..e181fe3e 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -302,12 +302,11 @@ extension to be approved you have to follow these guidelines: - it must be possible to use the factory pattern for creating applications. -4. The license has to be BSD/MIT/WTFPL licensed unless a depending - library absolutely enforces GPL or another license. +4. The license must be BSD/MIT/WTFPL licensed. 5. The naming scheme for official extensions is *Flask-ExtensionName* or *ExtensionName-Flask*. 6. Approved extensions must define all their dependencies in the - `setup.py` file unless a dependency cannot by met because it is not + `setup.py` file unless a dependency cannot be met because it is not available on PyPI. 7. The extension must have documentation that uses one of the two Flask themes for Sphinx documentation. From a993a314fe7be148747cf804167d45f2ea8bf18e Mon Sep 17 00:00:00 2001 From: Ali Afshar Date: Sun, 25 Jul 2010 18:45:10 +0800 Subject: [PATCH 0429/3747] Removed unused import --- flask/templating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/templating.py b/flask/templating.py index 41c0d39b..db78c3af 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -8,7 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound +from jinja2 import BaseLoader, TemplateNotFound from .globals import _request_ctx_stack from .signals import template_rendered From 3a80ecc66057d97e3a0ceb40e99ea7bde035381b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 17:33:45 +0200 Subject: [PATCH 0430/3747] Improved script for automatic extension testing --- Makefile | 5 +- tests/flaskext_test.py | 244 +++++++++++++++++++++++++++++++++++------ 2 files changed, 216 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index ba898b9e..8055e8ef 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc test upload-docs docs +.PHONY: clean-pyc ext-test test upload-docs docs all: clean-pyc test test: python setup.py test +ext-test: + python tests/flaskext_test.py --browse + release: python setup.py release sdist upload diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index aa4b7461..3614ad58 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -11,7 +11,14 @@ from __future__ import with_statement -import tempfile, subprocess, urllib2, os +import os +import sys +import shutil +import urllib2 +import tempfile +import subprocess +import argparse +from cStringIO import StringIO from flask import json @@ -19,24 +26,136 @@ from setuptools.package_index import PackageIndex from setuptools.archive_util import unpack_archive flask_svc_url = 'http://flask.pocoo.org/extensions/' -tdir = tempfile.mkdtemp() + +if sys.platform == 'darwin': + _tempdir = '/private/tmp' +else: + _tempdir = tempfile.gettempdir() +tdir = _tempdir + '/flaskext-test' +flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..')) -def run_tests(checkout_dir): - cmd = ['tox'] - return subprocess.call(cmd, cwd=checkout_dir, - stdout=open(os.path.join(tdir, 'tox.log'), 'w'), - stderr=subprocess.STDOUT) +RESULT_TEMPATE = u'''\ + +Flask-Extension Test Results + +

    Flask-Extension Test Results

    +

    + This page contains the detailed test results for the test run of + all {{ 'approved' if approved }} Flask extensions. +

    Summary

    + + + + + + + {%- for result in results %} + {% set outcome = 'success' if result.success else 'failed' %} + + + {%- endfor %} + +
    Extension + Version + Author + License + Outcome +
    {{ result.name }} + {{ result.version }} + {{ result.author }} + {{ result.license }} + {{ outcome }} +
    +

    Test Logs

    +

    Detailed test logs for all tests on all platforms: +{%- for result in results %} + {%- for iptr, log in result.logs|dictsort %} +

    {{ result.name }} - {{ result.version }} [{{ iptr }}]

    +
    {{ log }}
    + {%- endfor %} +{%- endfor %} +''' + + +def log(msg, *args): + print '[EXTTEST]', msg % args + + +class TestResult(object): + + def __init__(self, name, folder, statuscode, interpreters): + intrptr = os.path.join(folder, '.tox/%s/bin/python' + % interpreters[0]) + self.statuscode = statuscode + self.folder = folder + self.success = statuscode == 0 + + def fetch(field): + try: + c = subprocess.Popen([intrptr, 'setup.py', + '--' + field], cwd=folder, + stdout=subprocess.PIPE) + return c.communicate()[0].strip() + except OSError: + return '?' + self.name = name + self.license = fetch('license') + self.author = fetch('author') + self.version = fetch('version') + + self.logs = {} + for interpreter in interpreters: + logfile = os.path.join(folder, '.tox/%s/log/test.log' + % interpreter) + if os.path.isfile(logfile): + self.logs[interpreter] = open(logfile).read() + else: + self.logs[interpreter] = '' + + +def create_tdir(): + try: + shutil.rmtree(tdir) + except Exception: + pass + os.mkdir(tdir) + + +def package_flask(): + distfolder = tdir + '/.flask-dist' + c = subprocess.Popen(['python', 'setup.py', 'sdist', '--formats=gztar', + '--dist', distfolder], cwd=flaskdir) + c.wait() + return os.path.join(distfolder, os.listdir(distfolder)[0]) def get_test_command(checkout_dir): - files = set(os.listdir(checkout_dir)) - if 'Makefile' in files: + if os.path.isfile(checkout_dir + '/Makefile'): return 'make test' - elif 'conftest.py' in files: - return 'py.test' - else: - return 'nosetests' + return 'python setup.py test' def fetch_extensions_list(): @@ -47,50 +166,111 @@ def fetch_extensions_list(): yield ext -def checkout_extension(ext): - name = ext['name'] +def checkout_extension(name): + log('Downloading extension %s to temporary folder', name) root = os.path.join(tdir, name) os.mkdir(root) - checkout_path = PackageIndex().download(ext['name'], root) + checkout_path = PackageIndex().download(name, root) + unpack_archive(checkout_path, root) path = None for fn in os.listdir(root): path = os.path.join(root, fn) if os.path.isdir(path): break + log('Downloaded to %s', path) return path tox_template = """[tox] -envlist=py26 +envlist=%(env)s [testenv] -commands= -%s -downloadcache= -%s +deps=%(deps)s +commands=bash flaskext-runtest.sh {envlogdir}/test.log +downloadcache=%(cache)s """ -def create_tox_ini(checkout_path): + +def create_tox_ini(checkout_path, interpreters, flask_dep): tox_path = os.path.join(checkout_path, 'tox.ini') if not os.path.exists(tox_path): with open(tox_path, 'w') as f: - f.write(tox_template % (get_test_command(checkout_path), tdir)) + f.write(tox_template % { + 'env': ','.join(interpreters), + 'cache': tdir, + 'deps': flask_dep + }) -# XXX command line -only_approved = True -def test_all_extensions(only_approved=only_approved): +def iter_extensions(only_approved=True): for ext in fetch_extensions_list(): if ext['approved'] or not only_approved: - checkout_path = checkout_extension(ext) - create_tox_ini(checkout_path) - ret = run_tests(checkout_path) - yield ext['name'], ret + yield ext['name'] + + +def test_extension(name, interpreters, flask_dep): + checkout_path = checkout_extension(name) + log('Running tests with tox in %s', checkout_path) + + # figure out the test command and write a wrapper script. We + # can't write that directly into the tox ini because tox does + # not invoke the command from the shell so we have no chance + # to pipe the output into a logfile + test_command = get_test_command(checkout_path) + log('Test command: %s', test_command) + f = open(checkout_path + '/flaskext-runtest.sh', 'w') + f.write(test_command + ' &> "$1"\n') + f.close() + + create_tox_ini(checkout_path, interpreters, flask_dep) + rv = subprocess.call(['tox'], cwd=checkout_path) + return TestResult(name, checkout_path, rv, interpreters) + + +def run_tests(interpreters, only_approved=True): + results = {} + create_tdir() + log('Packaging Flask') + flask_dep = package_flask() + log('Running extension tests') + log('Temporary Environment: %s', tdir) + for name in iter_extensions(only_approved): + log('Testing %s', name) + result = test_extension(name, interpreters, flask_dep) + if result.success: + log('Extension test succeeded') + else: + log('Extension test failed') + results[name] = result + return results + + +def render_results(results, approved): + from jinja2 import Template + items = results.values() + items.sort(key=lambda x: x.name.lower()) + rv = Template(RESULT_TEMPATE, autoescape=True).render(results=items, + approved=approved) + fd, filename = tempfile.mkstemp(suffix='.html') + os.fdopen(fd, 'w').write(rv.encode('utf-8') + '\n') + return filename + def main(): - for name, ret in test_all_extensions(): - print name, ret + parser = argparse.ArgumentParser(description='Runs Flask extension tests') + parser.add_argument('--all', dest='all', action='store_true', + help='run against all extensions, not just approved') + parser.add_argument('--browse', dest='browse', action='store_true', + help='show browser with the result summary') + args = parser.parse_args() + + results = run_tests(['py26'], not args.all) + filename = render_results(results, not args.all) + if args.browse: + import webbrowser + webbrowser.open('file:///' + filename.lstrip('/')) + print 'Results written to', filename if __name__ == '__main__': From fee5bdafe84bfcec492c2643544373c71f1e82a8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 17:34:47 +0200 Subject: [PATCH 0431/3747] Code cleanup --- flask/app.py | 3 +-- flask/helpers.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index 151525e6..9b6b7836 100644 --- a/flask/app.py +++ b/flask/app.py @@ -11,7 +11,6 @@ from __future__ import with_statement -import os from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -20,7 +19,7 @@ from jinja2 import Environment from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError, NotFound +from werkzeug.exceptions import HTTPException, InternalServerError from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func diff --git a/flask/helpers.py b/flask/helpers.py index 39214a10..a2d951b4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,7 +15,6 @@ import posixpath import mimetypes from time import time from zlib import adler32 -from functools import wraps # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. From 312dfb43735dfaf063f58f8115c61331990302f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:51:28 +0200 Subject: [PATCH 0432/3747] Mentioned zip_safe --- docs/extensiondev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index e181fe3e..c2a6d2f7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -314,6 +314,8 @@ extension to be approved you have to follow these guidelines: link to the documentation, website (if there is one) and there must be a link to automatically install the development version (``PackageName==dev``). +9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, + even if the extension would be safe for zipping. .. _Flask Extension Wizard: From 9de61ea980edbd8fc3e2c459d3276b8f23d1428c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:51:36 +0200 Subject: [PATCH 0433/3747] Reviewed all extensions and wrote down notes. --- extreview/approved.rst | 32 +++++++ extreview/listed.rst | 183 +++++++++++++++++++++++++++++++++++++++++ extreview/rejected.rst | 55 +++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 extreview/approved.rst create mode 100644 extreview/listed.rst create mode 100644 extreview/rejected.rst diff --git a/extreview/approved.rst b/extreview/approved.rst new file mode 100644 index 00000000..852374db --- /dev/null +++ b/extreview/approved.rst @@ -0,0 +1,32 @@ +Approved Extensions +=================== + +This document contains a list of all extensions that were approved and the +date of approval as well as notes. This should make it possible to better +track the extension approval process. + + +Flask-Babel +----------- + +First Approval: 2010-07-23 +Last Review: 2010-07-23 +Approved version: 0.6 +Approved license: BSD + +Notes: Developed by the Flask development head + +How to improve: add a better long description to the next release + + +Flask-SQLAlchemy +---------------- + +First Approval: 2010-07-25 +Last Review: 2010-07-25 +Approved version: 0.9 +Approved license: BSD + +Notes: Developed by the Flask development head + +How to improve: add a better long description to the next release diff --git a/extreview/listed.rst b/extreview/listed.rst new file mode 100644 index 00000000..426e6ce4 --- /dev/null +++ b/extreview/listed.rst @@ -0,0 +1,183 @@ +Listed Extensions +================= + +This list contains extensions that passed listing. This means the +extension is on the list of extensions on the website. It does not +contain extensions that are approved. + + +Flask-CouchDB +------------- + +Last-Review: 2010-07-25 +Reviewed version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-CouchDBKit +---------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Creole +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the README +file is empty. + + +flask-csrf +---------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Will not be approved because this is functionality that should be handled +in the form handling systems which is for Flask-WTF already the case. +Also, this implementation only supports one open tab with forms. + +Name is not following Flask extension naming rules. + +Considered for unlisting. + + +Flask-Genshi +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.3 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the long +description is empty. The zip_safe flag is not set to False which is a +requirement for approved extensions. + + +flask-lesscss +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.9.1 + +Broken package description, nonconforming package name, does not follow +standard API rules (init_lesscss instead of lesscss). + +Considered for unlisting, improved version should release as +"Flask-LessCSS" with a conforming API and fixed packages indices, as well +as a testsuite. + + +flask-mail +---------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.3.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". Furthermore the long +description in the package index is a little bit too short. + +Package name should be changed to Flask-Mail with the approval to be +consistent, this might also be the change to improve the API if necessary, +but I don't see any big design problems there. + + +Flask-OAuth +----------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.9 + +Short long description, missing tests. + + +Flask-OpenID +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 1.0.1 + +Short long description, missing tests. + + +Flask-Script +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + +The upcoming 0.3 release looks promising, could need a longer "long +description" in the package index though. + + +Flask-Testing +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Themes +------------ + +Last-Review: 2010-07-25 +Reviewed Version: 0.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-Uploads +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-WTF +--------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2.1 + +Would be fine for approval, but the test suite is not part of the sdist +package (missing entry in MANIFEST.in) and the test suite does not respond +to either "make test" or "python setup.py test". + + +Flask-XML-RPC +------------- + +Last-Review: 2010-07-25 +Reviewed Version: 0.2.1 + +Missing tests, API wise it would be fine for approval. diff --git a/extreview/rejected.rst b/extreview/rejected.rst new file mode 100644 index 00000000..a953f363 --- /dev/null +++ b/extreview/rejected.rst @@ -0,0 +1,55 @@ +Rejected Extensions +=================== + +This is a list of extensions that is currently rejected from listing and +with that also not approved. If an extension ends up here it should +improved to be listed. + + +Flask-Actions +------------- + +Last Review: 2010-07-25 +Reviewed version: 0.2 + +Rejected because of missing description in PyPI, formatting issues with +the documentation (missing headlines, scrollbars etc.) and a general clash +of functionality with the Flask-Script package. Latter should not be a +problem, but the documentation should improve. For listing, the extension +developer should probably discuss the extension on the mailinglist with +others. + +Futhermore it also has an egg registered with an invalid filename. + + +Flask-Jinja2Extender +-------------------- + +Last Review: 2010-07-25 +Reviewed version: 0.1 + +Usecase not obvious, hacky implementation, does not solve a problem that +could not be solved with Flask itself. I suppose it is to aid other +extensions, but that should be discussed on the mailinglist. + + +Flask-Markdown +-------------- + +Last Review: 2010-07-25 +Reviewed version: 0.2 + +Would be great for enlisting but it should follow the API of Flask-Creole. +Besides that, the docstrings are not valid rst (run through rst2html to +see the issue) and it is missing tests. Otherwise fine :) + + +flask-urls +---------- + +Last Review: 2010-07-25 +Reviewed version: 0.9.2 + +Broken PyPI index and non-conforming extension name. Due to the small +featureset this was also delisted from the list. It was there previously +before the approval process was introduced. From 4c5c644fbf8e94096eb1a61d76855a98687cdb89 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 18:56:14 +0200 Subject: [PATCH 0434/3747] Renamed rejected to unlisted --- extreview/{rejected.rst => unlisted.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extreview/{rejected.rst => unlisted.rst} (100%) diff --git a/extreview/rejected.rst b/extreview/unlisted.rst similarity index 100% rename from extreview/rejected.rst rename to extreview/unlisted.rst From 2401d86008bb68f5f0368fbfe8d5797a8ef75903 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:00:00 +0200 Subject: [PATCH 0435/3747] Fixed formatting --- extreview/approved.rst | 16 +++++------ extreview/listed.rst | 60 +++++++++++++++++++++--------------------- extreview/unlisted.rst | 18 ++++++------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 852374db..a09aeb11 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -9,10 +9,10 @@ track the extension approval process. Flask-Babel ----------- -First Approval: 2010-07-23 -Last Review: 2010-07-23 -Approved version: 0.6 -Approved license: BSD +:First Approval: 2010-07-23 +:Last Review: 2010-07-23 +:Approved version: 0.6 +:Approved license: BSD Notes: Developed by the Flask development head @@ -22,10 +22,10 @@ How to improve: add a better long description to the next release Flask-SQLAlchemy ---------------- -First Approval: 2010-07-25 -Last Review: 2010-07-25 -Approved version: 0.9 -Approved license: BSD +:First Approval: 2010-07-25 +:Last Review: 2010-07-25 +:Approved version: 0.9 +:Approved license: BSD Notes: Developed by the Flask development head diff --git a/extreview/listed.rst b/extreview/listed.rst index 426e6ce4..cd658e92 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -9,8 +9,8 @@ contain extensions that are approved. Flask-CouchDB ------------- -Last-Review: 2010-07-25 -Reviewed version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -20,8 +20,8 @@ to either "make test" or "python setup.py test". Flask-CouchDBKit ---------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -31,8 +31,8 @@ to either "make test" or "python setup.py test". Flask-Creole ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -43,8 +43,8 @@ file is empty. flask-csrf ---------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Will not be approved because this is functionality that should be handled in the form handling systems which is for Flask-WTF already the case. @@ -58,8 +58,8 @@ Considered for unlisting. Flask-Genshi ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.3 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.3 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -71,8 +71,8 @@ requirement for approved extensions. flask-lesscss ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.9.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.9.1 Broken package description, nonconforming package name, does not follow standard API rules (init_lesscss instead of lesscss). @@ -85,8 +85,8 @@ as a testsuite. flask-mail ---------- -Last-Review: 2010-07-25 -Reviewed Version: 0.3.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.3.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -101,8 +101,8 @@ but I don't see any big design problems there. Flask-OAuth ----------- -Last-Review: 2010-07-25 -Reviewed Version: 0.9 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.9 Short long description, missing tests. @@ -110,8 +110,8 @@ Short long description, missing tests. Flask-OpenID ------------ -Last-Review: 2010-07-25 -Reviewed Version: 1.0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 1.0.1 Short long description, missing tests. @@ -119,8 +119,8 @@ Short long description, missing tests. Flask-Script ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -133,8 +133,8 @@ description" in the package index though. Flask-Testing ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -144,8 +144,8 @@ to either "make test" or "python setup.py test". Flask-Themes ------------ -Last-Review: 2010-07-25 -Reviewed Version: 0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -155,8 +155,8 @@ to either "make test" or "python setup.py test". Flask-Uploads ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -166,8 +166,8 @@ to either "make test" or "python setup.py test". Flask-WTF --------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2.1 Would be fine for approval, but the test suite is not part of the sdist package (missing entry in MANIFEST.in) and the test suite does not respond @@ -177,7 +177,7 @@ to either "make test" or "python setup.py test". Flask-XML-RPC ------------- -Last-Review: 2010-07-25 -Reviewed Version: 0.2.1 +:Last-Review: 2010-07-25 +:Reviewed Version: 0.2.1 Missing tests, API wise it would be fine for approval. diff --git a/extreview/unlisted.rst b/extreview/unlisted.rst index a953f363..6168c912 100644 --- a/extreview/unlisted.rst +++ b/extreview/unlisted.rst @@ -1,4 +1,4 @@ -Rejected Extensions +Unlisted Extensions =================== This is a list of extensions that is currently rejected from listing and @@ -9,8 +9,8 @@ improved to be listed. Flask-Actions ------------- -Last Review: 2010-07-25 -Reviewed version: 0.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.2 Rejected because of missing description in PyPI, formatting issues with the documentation (missing headlines, scrollbars etc.) and a general clash @@ -25,8 +25,8 @@ Futhermore it also has an egg registered with an invalid filename. Flask-Jinja2Extender -------------------- -Last Review: 2010-07-25 -Reviewed version: 0.1 +:Last Review: 2010-07-25 +:Reviewed version: 0.1 Usecase not obvious, hacky implementation, does not solve a problem that could not be solved with Flask itself. I suppose it is to aid other @@ -36,8 +36,8 @@ extensions, but that should be discussed on the mailinglist. Flask-Markdown -------------- -Last Review: 2010-07-25 -Reviewed version: 0.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.2 Would be great for enlisting but it should follow the API of Flask-Creole. Besides that, the docstrings are not valid rst (run through rst2html to @@ -47,8 +47,8 @@ see the issue) and it is missing tests. Otherwise fine :) flask-urls ---------- -Last Review: 2010-07-25 -Reviewed version: 0.9.2 +:Last Review: 2010-07-25 +:Reviewed version: 0.9.2 Broken PyPI index and non-conforming extension name. Due to the small featureset this was also delisted from the list. It was there previously From c6f55b5dbd1c5f08e56aa13b61b9b9912b5b4f21 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:02:02 +0200 Subject: [PATCH 0436/3747] Consistency --- extreview/approved.rst | 8 ++++---- extreview/unlisted.rst | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index a09aeb11..ea178ceb 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -11,8 +11,8 @@ Flask-Babel :First Approval: 2010-07-23 :Last Review: 2010-07-23 -:Approved version: 0.6 -:Approved license: BSD +:Approved Version: 0.6 +:Approved License: BSD Notes: Developed by the Flask development head @@ -24,8 +24,8 @@ Flask-SQLAlchemy :First Approval: 2010-07-25 :Last Review: 2010-07-25 -:Approved version: 0.9 -:Approved license: BSD +:Approved Version: 0.9 +:Approved License: BSD Notes: Developed by the Flask development head diff --git a/extreview/unlisted.rst b/extreview/unlisted.rst index 6168c912..6ee8b441 100644 --- a/extreview/unlisted.rst +++ b/extreview/unlisted.rst @@ -10,7 +10,7 @@ Flask-Actions ------------- :Last Review: 2010-07-25 -:Reviewed version: 0.2 +:Reviewed Version: 0.2 Rejected because of missing description in PyPI, formatting issues with the documentation (missing headlines, scrollbars etc.) and a general clash @@ -26,7 +26,7 @@ Flask-Jinja2Extender -------------------- :Last Review: 2010-07-25 -:Reviewed version: 0.1 +:Reviewed Version: 0.1 Usecase not obvious, hacky implementation, does not solve a problem that could not be solved with Flask itself. I suppose it is to aid other @@ -37,7 +37,7 @@ Flask-Markdown -------------- :Last Review: 2010-07-25 -:Reviewed version: 0.2 +:Reviewed Version: 0.2 Would be great for enlisting but it should follow the API of Flask-Creole. Besides that, the docstrings are not valid rst (run through rst2html to @@ -48,7 +48,7 @@ flask-urls ---------- :Last Review: 2010-07-25 -:Reviewed version: 0.9.2 +:Reviewed Version: 0.9.2 Broken PyPI index and non-conforming extension name. Due to the small featureset this was also delisted from the list. It was there previously From 385e67422ba1d590cf65e81dc7a85c916ada62e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 19:44:08 +0200 Subject: [PATCH 0437/3747] Reviewed 0.9.1 :) --- extreview/approved.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index ea178ceb..96490063 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -24,7 +24,7 @@ Flask-SQLAlchemy :First Approval: 2010-07-25 :Last Review: 2010-07-25 -:Approved Version: 0.9 +:Approved Version: 0.9.1 :Approved License: BSD Notes: Developed by the Flask development head From e3f440d80ffedcc2adb0f058e06a3ee6c56710d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:19:00 +0200 Subject: [PATCH 0438/3747] Approved Flask-Creole --- extreview/approved.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 96490063..526a181a 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -30,3 +30,15 @@ Flask-SQLAlchemy Notes: Developed by the Flask development head How to improve: add a better long description to the next release + + +Flask-Creole +------------ + +:First Approval: 2010-07-25 +:Last Review: 2010-07-25 +:Approved Version: 0.4.4 +:Approved License: BSD + +Notes: Flask-Markdown and this should share API, consider that when +approving Flask-Markdown From 54c126e7872b30dc9e5e28303eceea9e827e0231 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:25:15 +0200 Subject: [PATCH 0439/3747] Removed Flask-Creole from the listed file, it's in approved already --- extreview/listed.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/extreview/listed.rst b/extreview/listed.rst index cd658e92..a54c8a77 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -28,18 +28,6 @@ package (missing entry in MANIFEST.in) and the test suite does not respond to either "make test" or "python setup.py test". -Flask-Creole ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the README -file is empty. - - flask-csrf ---------- From d455135338d84dfac2a2b98f52cde1d43dd87681 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Jul 2010 23:46:24 +0200 Subject: [PATCH 0440/3747] Added a workaround for py.test --- tests/flaskext_test.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 3614ad58..c7eb37df 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -216,13 +216,22 @@ def test_extension(name, interpreters, flask_dep): # figure out the test command and write a wrapper script. We # can't write that directly into the tox ini because tox does # not invoke the command from the shell so we have no chance - # to pipe the output into a logfile + # to pipe the output into a logfile. The /dev/null hack is + # to trick py.test (if used) into not guessing widths from the + # invoking terminal. test_command = get_test_command(checkout_path) log('Test command: %s', test_command) f = open(checkout_path + '/flaskext-runtest.sh', 'w') - f.write(test_command + ' &> "$1"\n') + f.write(test_command + ' &> "$1" < /dev/null\n') f.close() + # if there is a tox.ini, remove it, it will cause troubles + # for us. Remove it if present, we are running tox ourselves + # afterall. + toxini = os.path.join(checkout_path, 'tox.ini') + if os.path.isfile(toxini): + os.remove(toxini) + create_tox_ini(checkout_path, interpreters, flask_dep) rv = subprocess.call(['tox'], cwd=checkout_path) return TestResult(name, checkout_path, rv, interpreters) From 63a37b75aca15085e685d99f8b7a2e79c83e1f57 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 00:36:09 +0200 Subject: [PATCH 0441/3747] Improved extension test runner --- tests/flaskext_test.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index c7eb37df..7a1f179b 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -74,6 +74,9 @@ RESULT_TEMPATE = u'''\ Author License Outcome + {%- for iptr, _ in results[0].logs|dictsort %} + {{ iptr }} + {%- endfor %} @@ -85,6 +88,9 @@ RESULT_TEMPATE = u'''\ {{ result.author }} {{ result.license }} {{ outcome }} + {%- for iptr, _ in result.logs|dictsort %} + see log + {%- endfor %} {%- endfor %} @@ -93,7 +99,8 @@ RESULT_TEMPATE = u'''\

    Detailed test logs for all tests on all platforms: {%- for result in results %} {%- for iptr, log in result.logs|dictsort %} -

    {{ result.name }} - {{ result.version }} [{{ iptr }}]

    +

    + {{ result.name }} - {{ result.version }} [{{ iptr }}]

    {{ log }}
    {%- endfor %} {%- endfor %} @@ -237,14 +244,14 @@ def test_extension(name, interpreters, flask_dep): return TestResult(name, checkout_path, rv, interpreters) -def run_tests(interpreters, only_approved=True): +def run_tests(extensions, interpreters): results = {} create_tdir() log('Packaging Flask') flask_dep = package_flask() log('Running extension tests') log('Temporary Environment: %s', tdir) - for name in iter_extensions(only_approved): + for name in extensions: log('Testing %s', name) result = test_extension(name, interpreters, flask_dep) if result.success: @@ -272,10 +279,21 @@ def main(): help='run against all extensions, not just approved') parser.add_argument('--browse', dest='browse', action='store_true', help='show browser with the result summary') + parser.add_argument('--env', dest='env', default='py25,py26,py27', + help='the tox environments to run against') + parser.add_argument('--extension=', dest='extension', default=None, + help='tests a single extension') args = parser.parse_args() - results = run_tests(['py26'], not args.all) - filename = render_results(results, not args.all) + if args.extension is not None: + only_approved = False + extensions = [args.extension] + else: + only_approved = not args.all + extensions = iter_extensions(only_approved) + + results = run_tests(extensions, [x.strip() for x in args.env.split(',')]) + filename = render_results(results, only_approved) if args.browse: import webbrowser webbrowser.open('file:///' + filename.lstrip('/')) From 140fc45ebd052ea07492c068e45d755d4b42c65c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 00:59:41 +0200 Subject: [PATCH 0442/3747] Added another workaround. the extension tester is now a pile of hacks --- tests/flaskext_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 7a1f179b..84f28b57 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -36,6 +36,10 @@ flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +# virtualenv hack *cough* +os.environ['PYTHONDONTWRITEBYTECODE'] = '' + + RESULT_TEMPATE = u'''\ Flask-Extension Test Results From 8bd8b014a8393d4b94e6fd192b64e3a2d2f65fce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:30:52 +0200 Subject: [PATCH 0443/3747] Small fixes in the extension tester --- tests/flaskext_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 84f28b57..6b669475 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -27,13 +27,16 @@ from setuptools.archive_util import unpack_archive flask_svc_url = 'http://flask.pocoo.org/extensions/' + +# OS X has awful paths when using mkstemp or gettempdir(). I don't +# care about security or clashes here, so pick something that is +# actually rememberable. if sys.platform == 'darwin': _tempdir = '/private/tmp' else: _tempdir = tempfile.gettempdir() tdir = _tempdir + '/flaskext-test' -flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), - '..')) +flaskdir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) # virtualenv hack *cough* From 4ab21cd7489e6a7b4e64beda7d8e44fd93557964 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:30:58 +0200 Subject: [PATCH 0444/3747] Approved Flask-Genshi --- extreview/approved.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 526a181a..a23f5cf8 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -42,3 +42,13 @@ Flask-Creole Notes: Flask-Markdown and this should share API, consider that when approving Flask-Markdown + + +Flask-Genshi +------------ + +:Last-Review: 2010-07-26 +:Reviewed Version: 0.3.1 + +Notes: This is the first template engine extension. When others come +around it would be a good idea to decide on a common interface. From c468ad0bfe8608d28e65a0a2c8ba1093dbd7167c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:37:16 +0200 Subject: [PATCH 0445/3747] Forgot to unlist flask-genshi --- extreview/listed.rst | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/extreview/listed.rst b/extreview/listed.rst index a54c8a77..c5b38b02 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -43,19 +43,6 @@ Name is not following Flask extension naming rules. Considered for unlisting. -Flask-Genshi ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.3 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the long -description is empty. The zip_safe flag is not set to False which is a -requirement for approved extensions. - - flask-lesscss ------------- From 6aeb6a09aff3115466eb39253ec236ec98348d9e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 02:52:05 +0200 Subject: [PATCH 0446/3747] Added standard dep on py because some extensions might use py.test and the default available version is on the wrong python path --- tests/flaskext_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 6b669475..11cb9fb0 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -200,7 +200,9 @@ tox_template = """[tox] envlist=%(env)s [testenv] -deps=%(deps)s +deps= + %(deps)s + py commands=bash flaskext-runtest.sh {envlogdir}/test.log downloadcache=%(cache)s """ From 89ed7e616e8b1bd16285703e44ecb40c953671cb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 13:20:46 +0200 Subject: [PATCH 0447/3747] Approved Flask-Script --- extreview/approved.rst | 18 ++++++++++++++++-- extreview/listed.rst | 14 -------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index a23f5cf8..0e404ac8 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -47,8 +47,22 @@ approving Flask-Markdown Flask-Genshi ------------ -:Last-Review: 2010-07-26 -:Reviewed Version: 0.3.1 +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.3.1 +:Approved License: BSD Notes: This is the first template engine extension. When others come around it would be a good idea to decide on a common interface. + + +Flask-Script +------------ + +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.3 +:Approved License: BSD + +Notes: Flask-Actions has some overlap. Consider that when approving +Flask-Actions or similar packages. diff --git a/extreview/listed.rst b/extreview/listed.rst index c5b38b02..f39a0e20 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -91,20 +91,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Script ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - -The upcoming 0.3 release looks promising, could need a longer "long -description" in the package index though. - - Flask-Testing ------------- From 27ce5cc0a139d06ed83bd60ace20ad38cdf4ec8f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 16:02:36 +0200 Subject: [PATCH 0448/3747] Added another rule to the approval list. 2.5-2.7 compatibility --- docs/extensiondev.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index c2a6d2f7..1505e69c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -316,6 +316,8 @@ extension to be approved you have to follow these guidelines: (``PackageName==dev``). 9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, even if the extension would be safe for zipping. +10. An extension currently has to support Python 2.5, 2.6 as well as + Python 2.7 .. _Flask Extension Wizard: From 3da165f2f56fe7f1b71242157fd4d02d3e37128f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 17:14:17 +0200 Subject: [PATCH 0449/3747] Fixed typo --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 1505e69c..44633b33 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -314,7 +314,7 @@ extension to be approved you have to follow these guidelines: link to the documentation, website (if there is one) and there must be a link to automatically install the development version (``PackageName==dev``). -9. The ``zip_safe`` flag in the setup scrip must be set to ``False``, +9. The ``zip_safe`` flag in the setup script must be set to ``False``, even if the extension would be safe for zipping. 10. An extension currently has to support Python 2.5, 2.6 as well as Python 2.7 From 2594602076e223f237a5cf4d0f8742ebc3ad3ab1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Jul 2010 17:14:25 +0200 Subject: [PATCH 0450/3747] Approved Flask-CouchDB --- extreview/approved.rst | 12 ++++++++++++ extreview/listed.rst | 16 ---------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 0e404ac8..59dc2e54 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -66,3 +66,15 @@ Flask-Script Notes: Flask-Actions has some overlap. Consider that when approving Flask-Actions or similar packages. + + +Flask-CouchDB +------------- + +:First Approval: 2010-07-26 +:Last Review: 2010-07-26 +:Approved Version: 0.2.1 +:Approved License: MIT + +There is also Flask-CouchDBKit. Both are fine because they are doing +different things, but the latter is not yet approved. diff --git a/extreview/listed.rst b/extreview/listed.rst index f39a0e20..c4a436c8 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -57,22 +57,6 @@ Considered for unlisting, improved version should release as as a testsuite. -flask-mail ----------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.3.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". Furthermore the long -description in the package index is a little bit too short. - -Package name should be changed to Flask-Mail with the approval to be -consistent, this might also be the change to improve the API if necessary, -but I don't see any big design problems there. - - Flask-OAuth ----------- From c18f032a822fa6db560c440d7faa7c615826aa30 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 01:29:21 +0200 Subject: [PATCH 0451/3747] Added a tox-test command that runs Flask tests with tox --- .gitignore | 1 + Makefile | 3 +++ tox.ini | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 4844be8e..5c012aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist *.egg *.egg-info _mailinglist +.tox diff --git a/Makefile b/Makefile index 8055e8ef..ab0a8d97 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ all: clean-pyc test test: python setup.py test +tox-test: + PYTHONDONTWRITEBYTECODE= tox + ext-test: python tests/flaskext_test.py --browse diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..15911b4c --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist=py25,py26,py27 + +[testenv] +commands=make test From 28f7c1956bc60a81be9a9e5935d72d25b444f3f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:10:42 +0200 Subject: [PATCH 0452/3747] Approved WTF and Testing --- extreview/approved.rst | 22 ++++++++++++++++++++++ extreview/listed.rst | 33 --------------------------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 59dc2e54..13db5e66 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -78,3 +78,25 @@ Flask-CouchDB There is also Flask-CouchDBKit. Both are fine because they are doing different things, but the latter is not yet approved. + + +Flask-Testing +------------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.2.3 +:Approved License: BSD + +All fine. + + +Flask-WTF +--------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.2.3 +:Approved License: BSD + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index c4a436c8..efd8571b 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -6,17 +6,6 @@ extension is on the list of extensions on the website. It does not contain extensions that are approved. -Flask-CouchDB -------------- - -:Last-Review: 2010-07-25 -:Reviewed version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-CouchDBKit ---------------- @@ -75,17 +64,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Testing -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-Themes ------------ @@ -108,17 +86,6 @@ package (missing entry in MANIFEST.in) and the test suite does not respond to either "make test" or "python setup.py test". -Flask-WTF ---------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-XML-RPC ------------- From 5cadd9d34da46b909f91a5379d41b90f258d5998 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:38:59 +0200 Subject: [PATCH 0453/3747] Picked codename for 0.6 --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c1f50998..54a65836 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.6 ----------- -Release date to be announced, codename to be decided. +Released on July 27th 2010, codename Whisky - after request functions are now called in reverse order of registration. From 16b101edb6c10a984b0f2960feeeca5a458845a1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 14:40:21 +0200 Subject: [PATCH 0454/3747] HEAD is 0.7-dev --- CHANGES | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 54a65836..a195fd1f 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,12 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.7 +----------- + +Release date to be announced, codename to be selected + + Version 0.6 ----------- diff --git a/setup.py b/setup.py index b97a33f7..78bd25d0 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run_tests(): setup( name='Flask', - version='0.6', + version='0.7', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 3e297e89d895262e4cf18bfa860f83fbe4b86a61 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 17:15:20 +0200 Subject: [PATCH 0455/3747] Approved Flask-Themes and Flask-Uploads --- extreview/approved.rst | 22 ++++++++++++++++++++++ extreview/listed.rst | 22 ---------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 13db5e66..78299933 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -100,3 +100,25 @@ Flask-WTF :Approved License: BSD All fine. + + +Flask-Themes +------------ + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. + + +Flask-Uploads +------------- + +:First Approval: 2010-07-27 +:Last Review: 2010-07-27 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index efd8571b..7510c8ac 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -64,28 +64,6 @@ Flask-OpenID Short long description, missing tests. -Flask-Themes ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - -Flask-Uploads -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.1 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - Flask-XML-RPC ------------- From 750c3a07d8a82bde3cf8d60c7ef29e26ef149a1c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Jul 2010 18:46:29 +0200 Subject: [PATCH 0456/3747] Added artwork to MANIFEST.in --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 166311d4..3fef8b5b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include Makefile CHANGES LICENSE AUTHORS +recursive-include artwork * recursive-include tests * recursive-include examples * recursive-include docs * From 3561a398b1a9ad4cfe7f056fe10811930f54b502 Mon Sep 17 00:00:00 2001 From: Aku Kotkavuo Date: Wed, 28 Jul 2010 05:27:10 +0800 Subject: [PATCH 0457/3747] Made one sentence easier to parse in docs/foreword --- docs/foreword.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 3a7521d4..308de9c5 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -40,8 +40,8 @@ was implemented in Flask itself. There are currently extensions for object relational mappers, form validation, upload handling, various open authentication technologies and more. -However Flask is not much code and built in a very solid foundation and -with that very easy to adapt for large applications. If you are +However Flask is not much code and it is built on a very solid foundation +and with that it is very easy to adapt for large applications. If you are interested in that, check out the :ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the From 4cd5201cdd03e14edbceb24620717bbe82300524 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 26 Jul 2010 14:45:05 +0800 Subject: [PATCH 0458/3747] use custom tox file named tox-flask-test.ini, dont delete the real tox.ini --- tests/flaskext_test.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 11cb9fb0..9dc9ad67 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -209,7 +209,7 @@ downloadcache=%(cache)s def create_tox_ini(checkout_path, interpreters, flask_dep): - tox_path = os.path.join(checkout_path, 'tox.ini') + tox_path = os.path.join(checkout_path, 'tox-flask-test.ini') if not os.path.exists(tox_path): with open(tox_path, 'w') as f: f.write(tox_template % { @@ -217,6 +217,7 @@ def create_tox_ini(checkout_path, interpreters, flask_dep): 'cache': tdir, 'deps': flask_dep }) + return tox_path def iter_extensions(only_approved=True): @@ -244,12 +245,9 @@ def test_extension(name, interpreters, flask_dep): # if there is a tox.ini, remove it, it will cause troubles # for us. Remove it if present, we are running tox ourselves # afterall. - toxini = os.path.join(checkout_path, 'tox.ini') - if os.path.isfile(toxini): - os.remove(toxini) create_tox_ini(checkout_path, interpreters, flask_dep) - rv = subprocess.call(['tox'], cwd=checkout_path) + rv = subprocess.call(['tox', '-c', 'tox-flask-test.ini'], cwd=checkout_path) return TestResult(name, checkout_path, rv, interpreters) From 0906f7d58930e047f7ec67290997b552e587e219 Mon Sep 17 00:00:00 2001 From: florentx Date: Wed, 28 Jul 2010 00:19:47 +0800 Subject: [PATCH 0459/3747] Typos. --- docs/_themes | 1 - docs/styleguide.rst | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 3d964b66..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 diff --git a/docs/styleguide.rst b/docs/styleguide.rst index 1387d4a6..ec699052 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -31,7 +31,7 @@ Continuing long statements: .order_by(MyModel.name.desc()) \ .limit(10) - If you break in a statement with parentheses or brances, align to the + If you break in a statement with parentheses or braces, align to the braces:: this_is_a_very_long(function_call, 'with many parameters', @@ -105,8 +105,8 @@ Yoda statements are a nogo: pass Comparisons: - - against arbitary types: ``==`` and ``!=`` - - against singletones with ``is`` and ``is not`` (eg: ``foo is not + - against arbitrary types: ``==`` and ``!=`` + - against singletons with ``is`` and ``is not`` (eg: ``foo is not None``) - never compare something with `True` or `False` (for example never do ``foo == False``, do ``not foo`` instead) @@ -125,7 +125,7 @@ Naming Conventions - Class names: ``CamelCase``, with acronyms kept uppercase (``HTTPWriter`` and not ``HttpWriter``) - Variable names: ``lowercase_with_underscores`` -- Method and functin names: ``lowercase_with_underscores`` +- Method and function names: ``lowercase_with_underscores`` - Constants: ``UPPERCASE_WITH_UNDERSCORES`` - precompiled regular expressions: ``name_re`` @@ -151,9 +151,9 @@ Docstrings Docstring conventions: All docstrings are formatted with reStructuredText as understood by Sphinx. Depending on the number of lines in the docstring, they are - layed out differently. If it's just one line, the closing tripple + layed out differently. If it's just one line, the closing triple quote is on the same line as the opening, otherwise the text is on - the same line as the opening quote and the tripple quote that closes + the same line as the opening quote and the triple quote that closes the string on its own line:: def foo(): From 8a14a875d26dc0923c4c52ed1e56eb112d81e723 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:26:15 +0200 Subject: [PATCH 0460/3747] Added the upcoming bugfix release to the mainbranches CHANGES --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index a195fd1f..0bd84823 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.7 Release date to be announced, codename to be selected +Version 0.6.1 +------------- + +Bugfix release, release date to be announced. Version 0.6 ----------- From dbf55de7e8bfcf7cde967aa72ea006b764d484ac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:25:08 +0200 Subject: [PATCH 0461/3747] Fixed an issue where the default `OPTIONS` response was not exposing all valid methods in the `Allow` header. This fixes #97 Signed-off-by: Armin Ronacher --- CHANGES | 3 +++ flask/app.py | 23 +++++++++++++++++++---- tests/flask_tests.py | 11 +++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 0bd84823..b9aa3739 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,9 @@ Version 0.6.1 Bugfix release, release date to be announced. +- Fixed an issue where the default `OPTIONS` response was + not exposing all valid methods in the `Allow` header. + Version 0.6 ----------- diff --git a/flask/app.py b/flask/app.py index 9b6b7836..72ed6292 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,8 @@ from jinja2 import Environment from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule -from werkzeug.exceptions import HTTPException, InternalServerError +from werkzeug.exceptions import HTTPException, InternalServerError, \ + MethodNotAllowed from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func @@ -689,14 +690,28 @@ class Flask(_PackageBoundObject): # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': - rv = self.response_class() - rv.allow.update(rule.methods) - return rv + return self._make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) + def _make_default_options_response(self): + # This would be nicer in Werkzeug 0.7, which however currently + # is not released. Werkzeug 0.7 provides a method called + # allowed_methods() that returns all methods that are valid for + # a given path. + methods = [] + try: + _request_ctx_stack.top.url_adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass + rv = self.response_class() + rv.allow.update(methods) + return rv + def make_response(self, rv): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3000e41c..93bdfef7 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -120,6 +120,17 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] assert rv.data == '' + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') From 738c66eff30d1a5f5672c22a32a02c29d9c38a2b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:32:39 +0200 Subject: [PATCH 0462/3747] Fixed submodules. Once again -.- --- docs/_themes | 1 + 1 file changed, 1 insertion(+) create mode 160000 docs/_themes diff --git a/docs/_themes b/docs/_themes new file mode 160000 index 00000000..3d964b66 --- /dev/null +++ b/docs/_themes @@ -0,0 +1 @@ +Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 From 43ae651c3091a9c1252726310abe5c9116ee2e53 Mon Sep 17 00:00:00 2001 From: Aku Kotkavuo Date: Wed, 28 Jul 2010 07:10:31 +0800 Subject: [PATCH 0463/3747] Proofread docs/quickstart --- docs/quickstart.rst | 161 ++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index d7cf5982..a85ed443 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,7 +3,7 @@ Quickstart ========== -Eager to get started? This page gives a good introduction in how to gets +Eager to get started? This page gives a good introduction in how to get started with Flask. This assumes you already have Flask installed. If you do not, head over to the :ref:`installation` section. @@ -37,14 +37,14 @@ see your hello world greeting. So what did that code do? -1. first we imported the :class:`~flask.Flask` class. An instance of this +1. First we imported the :class:`~flask.Flask` class. An instance of this class will be our WSGI application. The first argument is the name of the application's module. If you are using a single module (like here) you should use `__name__` because depending on if it's started as application or imported as module the name will be different (``'__main__'`` versus the actual import name). For more information on that, have a look at the :class:`~flask.Flask` documentation. -2. next we create an instance of it. We pass it the name of the module / +2. Next we create an instance of it. We pass it the name of the module / package. This is needed so that Flask knows where it should look for templates, static files and so on. 3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask @@ -81,7 +81,7 @@ To stop the server, hit control-C. Debug Mode ---------- -Now that :meth:`~flask.Flask.run` method is nice to start a local +The :meth:`~flask.Flask.run` method is nice to start a local development server, but you would have to restart it manually after each change you do to code. That is not very nice and Flask can do better. If you enable the debug support the server will reload itself on code changes @@ -101,11 +101,10 @@ Both will have exactly the same effect. .. admonition:: Attention - The interactive debugger however does not work in forking environments - which makes it nearly impossible to use on production servers but the - debugger still allows the execution of arbitrary code which makes it a - major security risk and **must never be used on production machines** - because of that. + Even though the interactive debugger does not work in forking environments + (which makes it nearly impossible to use on production servers), it still + allows the execution of arbitrary code. That makes it a major security + risk and therefore it **must never be used on production machines**. Screenshot of the debugger in action: @@ -118,11 +117,14 @@ Screenshot of the debugger in action: Routing ------- -As you have seen above, the :meth:`~flask.Flask.route` decorator is used -to bind a function to a URL. But there is more to it! You can make -certain parts of the URL dynamic and attach multiple rules to a function. +Modern web applications have beautiful URLs. This helps people remember +the URLs which is especially handy for applications that are used from +mobile devices with slower network connections. If the user can directly +go to the desired page without having to hit the index page it is more +likely he will like the page and come back next time. -Here some examples:: +As you have seen above, the :meth:`~flask.Flask.route` decorator is used +to bind a function to a URL. Here are some basic examples:: @app.route('/') def index(): @@ -132,20 +134,16 @@ Here some examples:: def hello(): return 'Hello World' +But there is more to it! You can make certain parts of the URL dynamic +and attach multiple rules to a function. Variable Rules `````````````` -Modern web applications have beautiful URLs. This helps people remember -the URLs which is especially handy for applications that are used from -mobile devices with slower network connections. If the user can directly -go to the desired page without having to hit the index page it is more -likely he will like the page and come back next time. - To add variable parts to a URL you can mark these special sections as ````. Such a part is then passed as keyword argument to your function. Optionally a converter can be specified by specifying a -rule with ````. Here some nice examples:: +rule with ````. Here are some nice examples:: @app.route('/user/') def show_user_profile(username): @@ -203,12 +201,12 @@ The following converters exist: URL Building ```````````` -If it can match URLs, can it also generate them? Of course you can. To +If it can match URLs, can it also generate them? Of course it can. To build a URL to a specific function you can use the :func:`~flask.url_for` function. It accepts the name of the function as first argument and a number of keyword arguments, each corresponding to the variable part of the URL rule. Unknown variable parts are appended to the URL as query -parameter. Here some examples: +parameter. Here are some examples: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -256,7 +254,7 @@ HTTP Methods HTTP (the protocol web applications are speaking) knows different methods to access URLs. By default a route only answers to `GET` requests, but that can be changed by providing the `methods` argument to the -:meth:`~flask.Flask.route` decorator. Here some examples:: +:meth:`~flask.Flask.route` decorator. Here are some examples:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -272,25 +270,24 @@ protocol) demands, so you can completely ignore that part of the HTTP specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you as well automatically. -You have no idea what an HTTP method is? Worry not, here quick -introduction in HTTP methods and why they matter: +You have no idea what an HTTP method is? Worry not, here is a quick +introduction to HTTP methods and why they matter: The HTTP method (also often called "the verb") tells the server what the clients wants to *do* with the requested page. The following methods are very common: `GET` - The Browser tells the server: just *get* me the information stored on - that page and send them to me. This is probably the most common - method. + The browser tells the server to just *get* the information stored on + that page and send it. This is probably the most common method. `HEAD` - The Browser tells the server: get me the information, but I am only + The browser tells the server to get the information, but it is only interested in the *headers*, not the content of the page. An application is supposed to handle that as if a `GET` request was - received but not deliver the actual contents. In Flask you don't have - to deal with that at all, the underlying Werkzeug library handles that - for you. + received but to not deliver the actual content. In Flask you don't + have to deal with that at all, the underlying Werkzeug library handles + that for you. `POST` The browser tells the server that it wants to *post* some new @@ -301,27 +298,27 @@ very common: `PUT` Similar to `POST` but the server might trigger the store procedure multiple times by overwriting the old values more than once. Now you - might be asking why this is any useful, but there are some good - reasons to do that. Consider the connection is lost during - transmission, in that situation a system between the browser and the - server might sent the request safely a second time without breaking + might be asking why is this useful, but there are some good reasons + to do it this way. Consider that the connection gets lost during + transmission: in this situation a system between the browser and the + server might receive the request safely a second time without breaking things. With `POST` that would not be possible because it must only be triggered once. `DELETE` - Remove the information that the given location. + Remove the information at the given location. `OPTIONS` - Provides a quick way for a requesting client to figure out which - methods are supported by this URL. Starting with Flask 0.6, this - is implemented for you automatically. + Provides a quick way for a client to figure out which methods are + supported by this URL. Starting with Flask 0.6, this is implemented + for you automatically. Now the interesting part is that in HTML4 and XHTML1, the only methods a -form might submit to the server are `GET` and `POST`. But with JavaScript -and future HTML standards you can use other methods as well. Furthermore -HTTP became quite popular lately and there are more things than browsers -that are speaking HTTP. (Your revision control system for instance might -speak HTTP) +form can submit to the server are `GET` and `POST`. But with JavaScript +and future HTML standards you can use the other methods as well. Furthermore +HTTP has become quite popular lately and browsers are no longer the only +clients that are using HTTP. For instance, many revision control system +use it. .. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt @@ -383,7 +380,7 @@ to the :ref:`templating` section of the documentation or the official `Jinja2 Template Documentation `_ for more information. -Here an example template: +Here is an example template: .. sourcecode:: html+jinja @@ -411,7 +408,7 @@ markup to HTML) you can mark it as safe by using the :class:`~jinja2.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. -Here a basic introduction in how the :class:`~jinja2.Markup` class works: +Here is a basic introduction to how the :class:`~jinja2.Markup` class works: >>> from flask import Markup >>> Markup('Hello %s!') % 'hacker' @@ -425,12 +422,12 @@ u'Marked up \xbb HTML' Autoescaping is no longer enabled for all templates. The following extensions for templates trigger autoescaping: ``.html``, ``.htm``, - ``.xml``, ``.xhtml``. Templates loaded from string will have + ``.xml``, ``.xhtml``. Templates loaded from a string will have autoescaping disabled. -.. [#] Unsure what that :class:`~flask.g` object is? It's something you - can store information on yourself, check the documentation of that - object (:class:`~flask.g`) and the :ref:`sqlite3` for more +.. [#] Unsure what that :class:`~flask.g` object is? It's something in which + you can store information for your own needs, check the documentation of + that object (:class:`~flask.g`) and the :ref:`sqlite3` for more information. @@ -454,10 +451,9 @@ Context Locals If you want to understand how that works and how you can implement tests with context locals, read this section, otherwise just skip it. -Certain objects in Flask are global objects, but not just a standard -global object, but actually a proxy to an object that is local to a -specific context. What a mouthful. But that is actually quite easy to -understand. +Certain objects in Flask are global objects, but not of the usual kind. +These objects are actually proxies to objects that are local to a specific +context. What a mouthful. But that is actually quite easy to understand. Imagine the context being the handling thread. A request comes in and the webserver decides to spawn a new thread (or something else, the @@ -469,13 +465,13 @@ It does that in an intelligent way that one application can invoke another application without breaking. So what does this mean to you? Basically you can completely ignore that -this is the case unless you are unittesting or something different. You +this is the case unless you are doing something like unittesting. You will notice that code that depends on a request object will suddenly break because there is no request object. The solution is creating a request object yourself and binding it to the context. The easiest solution for unittesting is by using the :meth:`~flask.Flask.test_request_context` context manager. In combination with the `with` statement it will bind a -test request so that you can interact with it. Here an example:: +test request so that you can interact with it. Here is an example:: from flask import request @@ -497,8 +493,8 @@ The Request Object `````````````````` The request object is documented in the API section and we will not cover -it here in detail (see :class:`~flask.request`), but just mention some of -the most common operations. First of all you have to import it from the +it here in detail (see :class:`~flask.request`). Here is a broad overview of +some of the most common operations. First of all you have to import it from the `flask` module:: from flask import request @@ -506,7 +502,7 @@ the `flask` module:: The current request method is available by using the :attr:`~flask.request.method` attribute. To access form data (data transmitted in a `POST` or `PUT` request) you can use the -:attr:`~flask.request.form` attribute. Here a full example of the two +:attr:`~flask.request.form` attribute. Here is a full example of the two attributes mentioned above:: @app.route('/login', methods=['POST', 'GET']) @@ -534,19 +530,18 @@ To access parameters submitted in the URL (``?key=value``) you can use the We recommend accessing URL parameters with `get` or by catching the `KeyError` because users might change the URL and presenting them a 400 -bad request page in that case is a bit user unfriendly. +bad request page in that case is not user friendly. -For a full list of methods and attributes on that object, head over to the -:class:`~flask.request` documentation. +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.request` documentation. File Uploads ```````````` -Obviously you can handle uploaded files with Flask just as easy. Just -make sure not to forget to set the ``enctype="multipart/form-data"`` -attribute on your HTML form, otherwise the browser will not transmit your -files at all. +You can handle uploaded files with Flask easily. Just make sure not to +forget to set the ``enctype="multipart/form-data"`` attribute on your HTML +form, otherwise the browser will not transmit your files at all. Uploaded files are stored in memory or at a temporary location on the filesystem. You can access those files by looking at the @@ -554,8 +549,8 @@ filesystem. You can access those files by looking at the uploaded file is stored in that dictionary. It behaves just like a standard Python :class:`file` object, but it also has a :meth:`~werkzeug.FileStorage.save` method that allows you to store that -file on the filesystem of the server. Here a simple example how that -works:: +file on the filesystem of the server. Here is a simple example showing how +that works:: from flask import request @@ -600,8 +595,8 @@ Redirects and Errors -------------------- To redirect a user to somewhere else you can use the -:func:`~flask.redirect` function, to abort a request early with an error -code the :func:`~flask.abort` function. Here an example how this works:: +:func:`~flask.redirect` function. To abort a request early with an error +code use the :func:`~flask.abort` function. Here an example how this works:: from flask import abort, redirect, url_for @@ -681,7 +676,7 @@ sessions work:: The here mentioned :func:`~flask.escape` does escaping for you if you are not using the template engine (like in this example). -.. admonition:: How to generate good Secret Keys +.. admonition:: How to generate good secret keys The problem with random is that it's hard to judge what random is. And a secret key should be as random as possible. Your operating system @@ -715,16 +710,17 @@ Logging .. versionadded:: 0.3 -Sometimes you might be in the situation where you deal with data that -should be correct, but actually is not. For example you have some client -side code that sends an HTTP request to the server, and it's obviously +Sometimes you might be in a situation where you deal with data that +should be correct, but actually is not. For example you may have some client +side code that sends an HTTP request to the server but it's obviously malformed. This might be caused by a user tempering with the data, or the -client code failed. Most the time, it's okay to reply with ``400 Bad -Request`` in that situation, but other times it is not and the code has to -continue working. +client code failing. Most of the time, it's okay to reply with ``400 Bad +Request`` in that situation, but sometimes that won't do and the code has +to continue working. -Yet you want to log that something fishy happened. This is where loggers -come in handy. As of Flask 0.3 a logger is preconfigured for you to use. +You may still want to log that something fishy happened. This is where +loggers come in handy. As of Flask 0.3 a logger is preconfigured for you +to use. Here are some example log calls:: @@ -733,8 +729,9 @@ Here are some example log calls:: app.logger.error('An error occurred') The attached :attr:`~flask.Flask.logger` is a standard logging -:class:`~logging.Logger`, so head over to the official stdlib -documentation for more information. +:class:`~logging.Logger`, so head over to the official `logging +documentation `_ for more +information. Hooking in WSGI Middlewares --------------------------- From 952967fcab0ffccc6b67485e8eed994a69d38d03 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 28 Jul 2010 01:39:25 +0200 Subject: [PATCH 0464/3747] In 0.7, make_default_options_response is a public API --- CHANGES | 4 ++++ flask/app.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index b9aa3739..03611cc8 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.7 Release date to be announced, codename to be selected +- Added :meth:`~flask.Flask.make_default_options_response` + which can be used by subclasses to alter the default + behaviour for `OPTIONS` responses. + Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index 72ed6292..faa5b168 100644 --- a/flask/app.py +++ b/flask/app.py @@ -690,13 +690,19 @@ class Flask(_PackageBoundObject): # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': - return self._make_default_options_response() + return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) except HTTPException, e: return self.handle_http_exception(e) - def _make_default_options_response(self): + def make_default_options_response(self): + """This method is called to create the default `OPTIONS` response. + This can be changed through subclassing to change the default + behaviour of `OPTIONS` responses. + + .. versionadded:: 0.7 + """ # This would be nicer in Werkzeug 0.7, which however currently # is not released. Werkzeug 0.7 provides a method called # allowed_methods() that returns all methods that are valid for From ed1b34c7249cc79c3287e5becbdfeca6d2155014 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 29 Jul 2010 11:03:56 +0200 Subject: [PATCH 0465/3747] Approved Flask-Mail --- extreview/approved.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 78299933..5e8699f5 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -122,3 +122,14 @@ Flask-Uploads :Approved License: MIT All fine. + + +Flask-Mail +---------- + +:First Approval: 2010-07-29 +:Last Review: 2010-07-29 +:Approved Version: 0.3.4 +:Approved License: BSD + +All fine. From 801918603cb075f96ff6c08a6fc6fb165f1eecda Mon Sep 17 00:00:00 2001 From: Stephane Wirtel Date: Thu, 29 Jul 2010 18:51:54 +0800 Subject: [PATCH 0466/3747] Remove an unused function --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index a2d951b4..53cee0d7 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -33,7 +33,7 @@ except ImportError: json_available = False -from werkzeug import Headers, wrap_file, is_resource_modified, cached_property +from werkzeug import Headers, wrap_file, cached_property from werkzeug.exceptions import NotFound from jinja2 import FileSystemLoader From 778e44e39eb42ff6cbee376ce40d1a52a25cb0ce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 30 Jul 2010 00:03:06 +0200 Subject: [PATCH 0467/3747] Improved error message for configuration files --- flask/config.py | 6 +++++- tests/flask_tests.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index cf9ad541..a27b362d 100644 --- a/flask/config.py +++ b/flask/config.py @@ -116,7 +116,11 @@ class Config(dict): filename = os.path.join(self.root_path, filename) d = type(sys)('config') d.__file__ = filename - execfile(filename, d.__dict__) + try: + execfile(filename, d.__dict__) + except IOError, e: + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise self.from_object(d) def from_object(self, obj): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 93bdfef7..010bc42e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1018,6 +1018,18 @@ class ConfigTestCase(unittest.TestCase): finally: os.environ = env + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.cfg'") + else: + assert 0, 'expected config' + class SubdomainTestCase(unittest.TestCase): From d09aa3765087bf6e8b8323008e4c1f160db2cd3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 30 Jul 2010 10:51:14 +0200 Subject: [PATCH 0468/3747] Approved Flask-XML-RPC --- extreview/approved.rst | 11 +++++++++++ extreview/listed.rst | 9 --------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/extreview/approved.rst b/extreview/approved.rst index 5e8699f5..58d18e72 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -133,3 +133,14 @@ Flask-Mail :Approved License: BSD All fine. + + +Flask-XML-RPC +------------- + +:First Approval: 2010-07-30 +:Last Review: 2010-07-30 +:Approved Version: 0.1.2 +:Approved License: MIT + +All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst index 7510c8ac..474213cb 100644 --- a/extreview/listed.rst +++ b/extreview/listed.rst @@ -62,12 +62,3 @@ Flask-OpenID :Reviewed Version: 1.0.1 Short long description, missing tests. - - -Flask-XML-RPC -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2.1 - -Missing tests, API wise it would be fine for approval. From ff2786d8afd6eba92ad75a22c2ad9275bd970f57 Mon Sep 17 00:00:00 2001 From: jgraeme Date: Tue, 3 Aug 2010 00:07:10 +0800 Subject: [PATCH 0469/3747] Fix some typos in the docs --- docs/extensiondev.rst | 8 ++++---- docs/patterns/errorpages.rst | 2 +- docs/patterns/jquery.rst | 8 ++++---- docs/patterns/lazyloading.rst | 2 +- docs/patterns/wtforms.rst | 4 ++-- docs/quickstart.rst | 2 +- docs/security.rst | 4 ++-- docs/upgrading.rst | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 44633b33..cfad85f0 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -67,7 +67,7 @@ First we create the following folder structure:: setup.py LICENSE -Here the contents of the most important files: +Here's the contents of the most important files: flaskext/__init__.py ```````````````````` @@ -171,7 +171,7 @@ controller object that can be used to connect to the database. The Extension Code ------------------ -Here the contents of the `flaskext/sqlite3.py` for copy/paste:: +Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: from __future__ import absolute_import import sqlite3 @@ -196,7 +196,7 @@ Here the contents of the `flaskext/sqlite3.py` for copy/paste:: g.sqlite3_db.close() return response -So here what the lines of code do: +So here's what the lines of code do: 1. the ``__future__`` import is necessary to activate absolute imports. This is needed because otherwise we could not call our module @@ -237,7 +237,7 @@ If you don't need that, you can go with initialization functions. Initialization Functions ------------------------ -Here how the module would look like with initialization functions:: +Here's what the module would look like with initialization functions:: from __future__ import absolute_import import sqlite3 diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index 5b4f6e80..95677644 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -5,7 +5,7 @@ Flask comes with a handy :func:`~flask.abort` function that aborts a request with an HTTP error code early. It will also provide a plain black and white error page for you with a basic description, but nothing fancy. -Depening on the error code it is less or more likely for the user to +Depending on the error code it is less or more likely for the user to actually see such an error. Common Error Codes diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index f2ca39c5..46285864 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -53,8 +53,8 @@ is quite simple: it's on localhost port something and directly on the root of that server. But what if you later decide to move your application to a different location? For example to ``http://example.com/myapp``? On the server side this never was a problem because we were using the handy -:func:`~flask.url_for` function that did could answer that question for -us, but if we are using jQuery we should better not hardcode the path to +:func:`~flask.url_for` function that could answer that question for +us, but if we are using jQuery we should not hardcode the path to the application but make that dynamic, so how can we do that? A simple method would be to add a script tag to our page that sets a @@ -118,9 +118,9 @@ special error reporting in that case. The HTML -------- -You index.html template either has to extend a `layout.html` template with +Your index.html template either has to extend a `layout.html` template with jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top. -Here the HTML code needed for our little application (`index.html`). +Here's the HTML code needed for our little application (`index.html`). Notice that we also drop the script directly into the HTML here. It is usually a better idea to have that in a separate script file: diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index b1fe4cbf..03b293d8 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -74,7 +74,7 @@ function but internally imports the real function on first use:: return self.view(*args, **kwargs) What's important here is is that `__module__` and `__name__` are properly -set. This is used by Flask internally to figure out how to do name the +set. This is used by Flask internally to figure out how to name the URL rules in case you don't provide a name for the rule yourself. Then you can define your central place to combine the views like this:: diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 8a113cc5..93824df7 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -77,7 +77,7 @@ how easy this is. WTForms does half the form generation for us already. To make it even nicer, we can write a macro that renders a field with label and a list of errors if there are any. -Here an example `_formhelpers.html` template with such a macro: +Here's an example `_formhelpers.html` template with such a macro: .. sourcecode:: html+jinja @@ -93,7 +93,7 @@ Here an example `_formhelpers.html` template with such a macro: {% endmacro %} This macro accepts a couple of keyword arguments that are forwarded to -WTForm's field function that renders the field for us. They keyword +WTForm's field function that renders the field for us. The keyword arguments will be inserted as HTML attributes. So for example you can call ``render_field(form.username, class='username')`` to add a class to the input element. Note that WTForms returns standard Python unicode diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a85ed443..c115fa70 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -231,7 +231,7 @@ parameter. Here are some examples: /user/John%20Doe (This also uses the :meth:`~flask.Flask.test_request_context` method -explained below. It basically tells flask to think we are handling a +explained below. It basically tells Flask to think we are handling a request even though we are not, we are in an interactive Python shell. Have a look at the explanation below. :ref:`context-locals`). diff --git a/docs/security.rst b/docs/security.rst index 45fff0ca..f3193d62 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -72,7 +72,7 @@ do stupid things without them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an -attacker now creates a page that sents a post request to that page with +attacker now creates a page that sends a post request to that page with some JavaScript he just has to trick some users to that page and their profiles will end up being deleted. @@ -163,6 +163,6 @@ page loaded the data from the JSON response is in the `captured` array. Because it is a syntax error in JavaScript to have an object literal (``{...}``) toplevel an attacker could not just do a request to an external URL with the script tag to load up the data. So what Flask does -is only allowing objects as toplevel elements when using +is to only allow objects as toplevel elements when using :func:`~flask.jsonify`. Make sure to do the same when using an ordinary JSON generate function. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 747fdb72..24966e7e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -2,7 +2,7 @@ Upgrading to Newer Releases =========================== Flask itself is changing like any software is changing over time. Most of -the changes are the nice kind, the kind where you don't have th change +the changes are the nice kind, the kind where you don't have to change anything in your code to profit from a new release. However every once in a while there are changes that do require some From 549af6229039e7fed7bcb2079737e45c27af998e Mon Sep 17 00:00:00 2001 From: jgraeme Date: Tue, 3 Aug 2010 00:18:19 +0800 Subject: [PATCH 0470/3747] Fix some typos in the docstrings --- flask/app.py | 4 ++-- flask/helpers.py | 2 +- flask/module.py | 4 ++-- flask/wrappers.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flask/app.py b/flask/app.py index faa5b168..f32e4dd8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -215,7 +215,7 @@ class Flask(_PackageBoundObject): #: A dictionary of all view functions registered. The keys will #: be function names which are also used to generate URLs and #: the values are the function objects themselves. - #: to register a view function, use the :meth:`route` decorator. + #: To register a view function, use the :meth:`route` decorator. self.view_functions = {} #: A dictionary of all registered error handlers. The key is @@ -242,7 +242,7 @@ class Flask(_PackageBoundObject): self.after_request_funcs = {} #: A dictionary with list of functions that are called without argument - #: to populate the template context. They key of the dictionary is the + #: to populate the template context. The key of the dictionary is the #: name of the module this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is #: updated with. To register a function here, use the diff --git a/flask/helpers.py b/flask/helpers.py index 53cee0d7..a6c18a33 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -146,7 +146,7 @@ def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. The endpoint is relative to the active module if modules are in use. - Here some examples: + Here are some examples: ==================== ======================= ============================= Active Module Target Endpoint Target Function diff --git a/flask/module.py b/flask/module.py index e6e1ee36..29350eb6 100644 --- a/flask/module.py +++ b/flask/module.py @@ -54,7 +54,7 @@ class Module(_PackageBoundObject): to be provided to keep them apart. If different import names are used, the rightmost part of the import name is used as name. - Here an example structure for a larger appliation:: + Here's an example structure for a larger application:: /myapplication /__init__.py @@ -73,7 +73,7 @@ class Module(_PackageBoundObject): app.register_module(admin, url_prefix='/admin') app.register_module(frontend) - And here an example view module (`myapplication/views/admin.py`):: + And here's an example view module (`myapplication/views/admin.py`):: from flask import Module diff --git a/flask/wrappers.py b/flask/wrappers.py index c578170c..4db1e782 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -17,7 +17,7 @@ from .globals import _request_ctx_stack class Request(RequestBase): - """The request object used by default in flask. Remembers the + """The request object used by default in Flask. Remembers the matched endpoint and view arguments. It is what ends up as :class:`~flask.request`. If you want to replace @@ -77,8 +77,8 @@ class Request(RequestBase): class Response(ResponseBase): - """The response object that is used by default in flask. Works like the - response object from Werkzeug but is set to have a HTML mimetype by + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by default. Quite often you don't have to create this object yourself because :meth:`~flask.Flask.make_response` will take care of that for you. From f52f4fd31b9858f8b42be3fbb2818440e58978b4 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Sun, 1 Aug 2010 15:04:09 +0800 Subject: [PATCH 0471/3747] Added initial version of 'setup.py audit' --- setup.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 78bd25d0..d2a9266b 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,44 @@ Links `_ """ -from setuptools import setup +from setuptools import Command, setup +class run_audit(Command): + """Audits source code using PyFlakes for following issues: + - Names which are used but not defined or used before they are defined. + - Names which are redefined without having been used. + """ + description = "Audit source code with PyFlakes" + user_options = [] + + def initialize_options(self): + all = None + + def finalize_options(self): + pass + + def run(self): + import os, sys + try: + import pyflakes.scripts.pyflakes as flakes + except ImportError: + print "Audit requires PyFlakes installed in your system.""" + sys.exit(-1) + + dirs = ['flask', 'tests'] + # Add example directories + for dir in ['flaskr', 'jqueryexample', 'minitwit']: + dirs.append(os.path.join('examples', dir)) + # TODO: Add test subdirectories + warns = 0 + for dir in dirs: + for filename in os.listdir(dir): + if filename.endswith('.py') and filename != '__init__.py': + warns += flakes.checkPath(os.path.join(dir, filename)) + if warns > 0: + print ("Audit finished with total %d warnings." % warns) + else: + print ("No problems found in sourcecode.") def run_tests(): import os, sys @@ -75,5 +111,6 @@ setup( 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], + cmdclass={'audit': run_audit}, test_suite='__main__.run_tests' ) From c9002569c9abfa715e491aa636555e51ddf85d50 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 3 Aug 2010 12:15:15 +0200 Subject: [PATCH 0472/3747] Various pyflakes fixes --- Makefile | 5 ++++- tests/flask_tests.py | 23 +++++++++++------------ tests/flaskext_test.py | 1 - 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index ab0a8d97..a0127457 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc ext-test test upload-docs docs +.PHONY: clean-pyc ext-test test upload-docs docs audit all: clean-pyc test test: python setup.py test +audit: + python setup.py audit + tox-test: PYTHONDONTWRITEBYTECODE= tox diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 010bc42e..2944f0ea 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -15,11 +15,10 @@ import re import sys import flask import unittest -import tempfile from logging import StreamHandler from contextlib import contextmanager from datetime import datetime -from werkzeug import parse_date, parse_options_header, http_date +from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -352,7 +351,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): called.append(4) return response @app.after_request - def after1(response): + def after2(response): called.append(3) return response @app.route('/') @@ -638,13 +637,13 @@ class ModuleTestCase(unittest.TestCase): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin', url_prefix='/admin') @admin.route('/') - def index(): + def admin_index(): return 'admin index' @admin.route('/login') - def login(): + def admin_login(): return 'admin login' @admin.route('/logout') - def logout(): + def admin_logout(): return 'admin logout' @app.route('/') def index(): @@ -680,7 +679,7 @@ class ModuleTestCase(unittest.TestCase): catched.append('after-admin') return response @admin.route('/') - def index(): + def admin_index(): return 'the admin' @app.before_request def before_request(): @@ -719,7 +718,7 @@ class ModuleTestCase(unittest.TestCase): def index(): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') @admin.route('/') - def index(): + def admin_index(): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') app.register_module(admin) c = app.test_client() @@ -794,13 +793,13 @@ class ModuleTestCase(unittest.TestCase): f = app.view_functions['admin.static'] try: - rv = f('/etc/passwd') + f('/etc/passwd') except NotFound: pass else: assert 0, 'expected exception' try: - rv = f('../__init__.py') + f('../__init__.py') except NotFound: pass else: @@ -914,7 +913,7 @@ class LoggingTestCase(unittest.TestCase): c = app.test_client() with catch_stderr() as err: - rv = c.get('/') + c.get('/') out = err.getvalue() assert 'WARNING in flask_tests [' in out assert 'flask_tests.py' in out @@ -1098,7 +1097,7 @@ class TestSignals(unittest.TestCase): flask.template_rendered.connect(record, app) try: - rv = app.test_client().get('/') + app.test_client().get('/') assert len(recorded) == 1 template, context = recorded[0] assert template.name == 'simple_template.html' diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 9dc9ad67..36a62694 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -18,7 +18,6 @@ import urllib2 import tempfile import subprocess import argparse -from cStringIO import StringIO from flask import json From d17b6d738a0e9f8a3dcf4996a65de6e3347acae6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 3 Aug 2010 12:17:36 +0200 Subject: [PATCH 0473/3747] Fixed a refacotring error in the docs. This fixes #100 --- docs/testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index d131c673..1a3a2960 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -43,13 +43,13 @@ In order to test that, we add a second module ( class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.db_fd, flaskr.DATABASE = tempfile.mkstemp() + self.db_fd, self.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() def tearDown(self): os.close(self.db_fd) - os.unlink(flaskr.DATABASE) + os.unlink(flaskr.app.config['DATABASE']) if __name__ == '__main__': unittest.main() From faa1c71e455a99e9b098aa9bb4667c07a1bab6aa Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:02:53 +0200 Subject: [PATCH 0474/3747] Request local objects now fail properly with a RuntimeError. This fixes #105 --- CHANGES | 2 ++ docs/upgrading.rst | 8 ++++++++ flask/globals.py | 15 +++++++++++---- tests/flask_tests.py | 6 +++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 03611cc8..6d0a0172 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,8 @@ Release date to be announced, codename to be selected - Added :meth:`~flask.Flask.make_default_options_response` which can be used by subclasses to alter the default behaviour for `OPTIONS` responses. +- Unbound locals now raise a proper :exc:`RuntimeError` instead + of an :exc:`AttributeError`. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 24966e7e..ba8e9947 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,14 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.7 +----------- + +Due to a bug in earlier implementations the request local proxies now +raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they +are unbound. If you cought these exceptions with :exc:`AttributeError` +before, you should catch them with :exc:`RuntimeError` now. + Version 0.6 ----------- diff --git a/flask/globals.py b/flask/globals.py index aac46555..84714105 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -10,11 +10,18 @@ :license: BSD, see LICENSE for more details. """ +from functools import partial from werkzeug import LocalStack, LocalProxy +def _lookup_object(name): + top = _request_ctx_stack.top + if top is None: + raise RuntimeError('working outside of request context') + return getattr(top, name) + # context locals _request_ctx_stack = LocalStack() -current_app = LocalProxy(lambda: _request_ctx_stack.top.app) -request = LocalProxy(lambda: _request_ctx_stack.top.request) -session = LocalProxy(lambda: _request_ctx_stack.top.session) -g = LocalProxy(lambda: _request_ctx_stack.top.g) +current_app = LocalProxy(partial(_lookup_object, 'app')) +request = LocalProxy(partial(_lookup_object, 'request')) +session = LocalProxy(partial(_lookup_object, 'session')) +g = LocalProxy(partial(_lookup_object, 'g')) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2944f0ea..2f90a2f3 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -72,7 +72,7 @@ class ContextTestCase(unittest.TestCase): ctx.pop() try: index() - except AttributeError: + except RuntimeError: pass else: assert 0, 'expected runtime error' @@ -469,6 +469,10 @@ class BasicFunctionalityTestCase(unittest.TestCase): else: assert "Expected ValueError" + def test_request_locals(self): + self.assertEqual(repr(flask.g), '') + self.assertFalse(flask.g) + class JSONTestCase(unittest.TestCase): From fda14678c07d036ef3a1984a4e346e793cc5a63c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:36:39 +0200 Subject: [PATCH 0475/3747] Deprecated send_file etag support and mimetype guessing for file-like objects. This fixes #104 --- CHANGES | 4 ++ docs/upgrading.rst | 17 ++++++++ flask/helpers.py | 27 +++++++++++- tests/flask_tests.py | 100 +++++++++++++++++++++++++++++-------------- 4 files changed, 116 insertions(+), 32 deletions(-) diff --git a/CHANGES b/CHANGES index 6d0a0172..bb5b295d 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Release date to be announced, codename to be selected behaviour for `OPTIONS` responses. - Unbound locals now raise a proper :exc:`RuntimeError` instead of an :exc:`AttributeError`. +- Mimetype guessing and etag support based on file objects is now + deprecated for :func:`flask.send_file` because it was unreliable. + Pass filenames instead or attach your own etags and provide a + proper mimetype by hand. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index ba8e9947..ac811e41 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -27,6 +27,23 @@ raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they are unbound. If you cought these exceptions with :exc:`AttributeError` before, you should catch them with :exc:`RuntimeError` now. +Additionally the :func:`~flask.send_file` function is now issuing +deprecation warnings if you depend on functionality that will be removed +in Flask 1.0. Previously it was possible to use etags and mimetypes +when file objects were passed. This was unreliable and caused issues +for a few setups. If you get a deprecation warning, make sure to +update your application to work with either filenames there or disable +etag attaching and attach them yourself. + +Old code:: + + return send_file(my_file_object) + return send_file(my_file_object) + +New code:: + + return send_file(my_file_object, add_etags=False) + Version 0.6 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index a6c18a33..18eb6d0e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -259,7 +259,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, By default it will try to guess the mimetype for you, but you can also explicitly provide one. For extra security you probably want - to sent certain files as attachment (HTML for instance). + to sent certain files as attachment (HTML for instance). The mimetype + guessing requires a `filename` or an `attachment_filename` to be + provided. Please never pass filenames to this function from user sources without checking them first. Something like this is usually sufficient to @@ -274,6 +276,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, The `add_etags`, `cache_timeout` and `conditional` parameters were added. The default behaviour is now to attach etags. + .. versionchanged:: 0.7 + mimetype guessing and etag support for file objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. This functionality + will be removed in Flask 1.0 + :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. @@ -295,8 +303,25 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, filename = filename_or_fp file = None else: + from warnings import warn file = filename_or_fp filename = getattr(file, 'name', None) + + # XXX: this behaviour is now deprecated because it was unreliable. + # removed in Flask 1.0 + if not attachment_filename and not mimetype \ + and isinstance(filename, basestring): + warn(DeprecationWarning('The filename support for file objects ' + 'passed to send_file is not deprecated. Pass an ' + 'attach_filename if you want mimetypes to be guessed.'), + stacklevel=2) + if add_etags: + warn(DeprecationWarning('In future flask releases etags will no ' + 'longer be generated for file objects passed to the send_file ' + 'function because this behaviour was unreliable. Pass ' + 'filenames instead if possible, otherwise attach an etag ' + 'yourself based on another value'), stacklevel=2) + if filename is not None: if not os.path.isabs(filename): filename = os.path.join(current_app.root_path, filename) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2f90a2f3..392368e7 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -33,8 +33,27 @@ TEST_KEY = 'foo' SECRET_KEY = 'devkey' +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + import warnings + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + @contextmanager def catch_stderr(): + """Catch stderr in a StringIO""" old_stderr = sys.stderr sys.stderr = rv = StringIO() try: @@ -834,46 +853,64 @@ class SendfileTestCase(unittest.TestCase): def test_send_file_object(self): app = flask.Flask(__name__) - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - assert rv.mimetype == 'text/html' + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + assert rv.data == f.read() + assert rv.mimetype == 'text/html' + # mimetypes + etag + assert len(captured) == 2 app.use_x_sendfile = True - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - assert rv.mimetype == 'text/html' - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + assert rv.mimetype == 'text/html' + assert 'x-sendfile' in rv.headers + assert rv.headers['x-sendfile'] == \ + os.path.join(app.root_path, 'static/index.html') + # mimetypes + etag + assert len(captured) == 2 app.use_x_sendfile = False with app.test_request_context(): - f = StringIO('Test') - rv = flask.send_file(f) - assert rv.data == 'Test' - assert rv.mimetype == 'application/octet-stream' - f = StringIO('Test') - rv = flask.send_file(f, mimetype='text/plain') - assert rv.data == 'Test' - assert rv.mimetype == 'text/plain' + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + assert rv.data == 'Test' + assert rv.mimetype == 'application/octet-stream' + # etags + assert len(captured) == 1 + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + assert rv.data == 'Test' + assert rv.mimetype == 'text/plain' + # etags + assert len(captured) == 1 app.use_x_sendfile = True - with app.test_request_context(): - f = StringIO('Test') - rv = flask.send_file(f) - assert 'x-sendfile' not in rv.headers + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + assert 'x-sendfile' not in rv.headers + # etags + assert len(captured) == 1 def test_attachment(self): app = flask.Flask(__name__) - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f, as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + # mimetypes + etag + assert len(captured) == 2 with app.test_request_context(): assert options['filename'] == 'index.html' @@ -884,7 +921,8 @@ class SendfileTestCase(unittest.TestCase): with app.test_request_context(): rv = flask.send_file(StringIO('Test'), as_attachment=True, - attachment_filename='index.txt') + attachment_filename='index.txt', + add_etags=False) assert rv.mimetype == 'text/plain' value, options = parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' From a354c393aed6d87ad62369cca046f0aff70254c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:37:43 +0200 Subject: [PATCH 0476/3747] Fixed a typo --- docs/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index ac811e41..91da2b78 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -24,7 +24,7 @@ Version 0.7 Due to a bug in earlier implementations the request local proxies now raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they -are unbound. If you cought these exceptions with :exc:`AttributeError` +are unbound. If you caught these exceptions with :exc:`AttributeError` before, you should catch them with :exc:`RuntimeError` now. Additionally the :func:`~flask.send_file` function is now issuing From c41a1cd8dca1110ca22da93fb80111b29c7e5740 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:38:26 +0200 Subject: [PATCH 0477/3747] Another typo fix --- docs/patterns/errorpages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index 95677644..d7852549 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -49,7 +49,7 @@ An error handler is a function, just like a view function, but it is called when an error happens and is passed that error. The error is most likely a :exc:`~werkzeug.exceptions.HTTPException`, but in one case it can be a different error: a handler for internal server errors will be -passed other exception instances as well if they are uncought. +passed other exception instances as well if they are uncaught. An error handler is registered with the :meth:`~flask.Flask.errorhandler` decorator and the error code of the exception. Keep in mind that Flask From 38107c752cf959ad69cf4f35886946bc947f2bd3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 7 Aug 2010 13:41:06 +0200 Subject: [PATCH 0478/3747] Fixed a wrong import path in the documentation. Fixes #102 --- docs/errorhandling.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 82d53b7c..c216c160 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -89,7 +89,7 @@ There are a couple of handlers provided by the logging system out of the box but not all of them are useful for basic error logging. The most interesting are probably the following: -- :class:`~logging.handlers.FileHandler` - logs messages to a file on the +- :class:`~logging.FileHandler` - logs messages to a file on the filesystem. - :class:`~logging.handlers.RotatingFileHandler` - logs messages to a file on the filesystem and will rotate after a certain number of messages. @@ -105,7 +105,7 @@ above, just make sure to use a lower setting (I would recommend if not app.debug: import logging - from logging.handlers import TheHandlerYouWant + from themodule import TheHandler YouWant file_handler = TheHandlerYouWant(...) file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) From a3a843999b8f98a96fb44f573515d44648d72bab Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Aug 2010 15:16:02 +0200 Subject: [PATCH 0479/3747] normpath is now used before loading templates --- CHANGES | 3 +++ flask/templating.py | 4 ++++ tests/flask_tests.py | 2 ++ tests/moduleapp/apps/admin/__init__.py | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/CHANGES b/CHANGES index bb5b295d..c20a3304 100644 --- a/CHANGES +++ b/CHANGES @@ -25,6 +25,9 @@ Bugfix release, release date to be announced. - Fixed an issue where the default `OPTIONS` response was not exposing all valid methods in the `Allow` header. +- Jinja2 template loading syntax now allows "./" in front of + a template load path. Previously this caused issues with + module setups. Version 0.6 ----------- diff --git a/flask/templating.py b/flask/templating.py index db78c3af..4db03b75 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -8,6 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import posixpath from jinja2 import BaseLoader, TemplateNotFound from .globals import _request_ctx_stack @@ -36,6 +37,9 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): + template = posixpath.normpath(template) + if template.startswith('../'): + raise TemplateNotFound(template) loader = None try: module, name = template.split('/', 1) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 392368e7..ae972d05 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -789,6 +789,8 @@ class ModuleTestCase(unittest.TestCase): assert rv.data == 'Hello from the Frontend' rv = c.get('/admin/') assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' rv = c.get('/admin/static/test.txt') assert rv.data.strip() == 'Admin File' rv = c.get('/admin/static/css/test.css') diff --git a/tests/moduleapp/apps/admin/__init__.py b/tests/moduleapp/apps/admin/__init__.py index 98af2b26..b85b8024 100644 --- a/tests/moduleapp/apps/admin/__init__.py +++ b/tests/moduleapp/apps/admin/__init__.py @@ -7,3 +7,8 @@ admin = Module(__name__, url_prefix='/admin') @admin.route('/') def index(): return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') From 9a21c34bb63c2343b7c8c6bcfc51fc72030909ed Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 10 Aug 2010 22:55:30 +0200 Subject: [PATCH 0480/3747] Added another testcase --- tests/flask_tests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ae972d05..b850dc8c 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -248,7 +248,13 @@ class BasicFunctionalityTestCase(unittest.TestCase): flask.session['test'] = 42 flask.session.permanent = permanent return '' - rv = app.test_client().get('/') + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') assert 'set-cookie' in rv.headers match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) @@ -257,6 +263,9 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert expires.month == expected.month assert expires.day == expected.day + rv = client.get('/test') + assert rv.data == 'True' + permanent = False rv = app.test_client().get('/') assert 'set-cookie' in rv.headers From 6b5ba145213d49078dc03f3f62fb3ad5aa139e68 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 10 Aug 2010 23:10:55 +0200 Subject: [PATCH 0481/3747] Added a missing is --- docs/patterns/flashing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index b491712f..4cea0206 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -14,7 +14,7 @@ template that does this. Simple Flashing --------------- -So here a full example:: +So here is a full example:: from flask import flash, redirect, url_for, render_template From a3f78af87019bfd89063cf5bc0c19f27325f36c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 13 Aug 2010 23:37:57 +0200 Subject: [PATCH 0482/3747] Improved the templated decorator in the documentation as recommended by Thadeus Burgess on the mailinglist --- docs/patterns/viewdecorators.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 49620ff8..73d67852 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -120,7 +120,9 @@ As you can see, if no template name is provided it will use the endpoint of the URL map with dots converted to slashes + ``'.html'``. Otherwise the provided template name is used. When the decorated function returns, the dictionary returned is passed to the template rendering function. If -`None` is returned, an empty dictionary is assumed. +`None` is returned, an empty dictionary is assumed, if something else than +a dictionary is returned we return it from the function unchanged. That +way you can still use the redirect function or return simple strings. Here the code for that decorator:: @@ -138,6 +140,8 @@ Here the code for that decorator:: ctx = f(*args, **kwargs) if ctx is None: ctx = {} + elif not isinstance(ctx, dict): + return ctx return render_template(template_name, **ctx) return decorated_function return decorator From 36a421bb3a235f696131bc8a546492fc04a411f0 Mon Sep 17 00:00:00 2001 From: Zhao Xiaohong Date: Sun, 15 Aug 2010 10:52:11 +0800 Subject: [PATCH 0483/3747] Fixed template_rendered example in signal documentation. --- docs/signals.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index feb9a7b2..73a0c744 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -55,7 +55,7 @@ to the template:: @contextmanager def captured_templates(app): recorded = [] - def record(template, context): + def record(sender, template, context): recorded.append((template, context)) template_rendered.connect(record, app) try: @@ -87,7 +87,7 @@ its own which simplifies the example above:: def captured_templates(app): recorded = [] - def record(template, context): + def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) @@ -155,7 +155,7 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new from flask import template_rendered @template_rendered.connect_via(app) - def when_template_rendered(template, context): + def when_template_rendered(sender, template, context): print 'Template %s is rendered with %s' % (template.name, context) Core Signals From 0566abc28da9c997a9f7ac7b38066075a7a81cd8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 18 Aug 2010 20:10:47 +0200 Subject: [PATCH 0484/3747] Fixed testing example --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index 1a3a2960..790eddf6 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -43,7 +43,7 @@ In order to test that, we add a second module ( class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.db_fd, self.app.config['DATABASE'] = tempfile.mkstemp() + self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() self.app = flaskr.app.test_client() flaskr.init_db() From d903f55e835dd143afb6e0079376525718ffb5a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 18 Aug 2010 21:58:04 +0200 Subject: [PATCH 0485/3747] s/sdist/dist/. This fixes #106 --- docs/patterns/fabric.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index de23ffa8..49be85ab 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -50,7 +50,7 @@ virtual environment:: # figure out the release name and version dist = local('python setup.py --fullname').strip() # upload the source tarball to the temporary folder on the server - put('sdist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') + put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it run('mkdir yourapplication') From 6fca662c841797a2006999c699a6738b20af1d4e Mon Sep 17 00:00:00 2001 From: Heungsub Lee Date: Fri, 20 Aug 2010 15:29:13 +0800 Subject: [PATCH 0486/3747] Fix the 108th issue. --- flask/module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/module.py b/flask/module.py index 29350eb6..5f3c8c1f 100644 --- a/flask/module.py +++ b/flask/module.py @@ -31,7 +31,8 @@ def _register_module(module, static_path): path = state.url_prefix + path state.app.add_url_rule(path + '/', endpoint='%s.static' % module.name, - view_func=module.send_static_file) + view_func=module.send_static_file, + subdomain=module.subdomain) return _register From 2a73bbc436041875f81aeffd5b8f13c6da8fcf19 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 20 Aug 2010 11:16:18 +0200 Subject: [PATCH 0487/3747] Added testcase. This fixes #108 --- tests/flask_tests.py | 9 +++++++++ tests/subdomaintestmodule/__init__.py | 4 ++++ tests/subdomaintestmodule/static/hello.txt | 1 + 3 files changed, 14 insertions(+) create mode 100644 tests/subdomaintestmodule/__init__.py create mode 100644 tests/subdomaintestmodule/static/hello.txt diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b850dc8c..8eecafc0 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1102,6 +1102,15 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + assert rv.data.strip() == 'Hello Subdomain' + def test_subdomain_matching(self): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'localhost' diff --git a/tests/subdomaintestmodule/__init__.py b/tests/subdomaintestmodule/__init__.py new file mode 100644 index 00000000..3c5e3583 --- /dev/null +++ b/tests/subdomaintestmodule/__init__.py @@ -0,0 +1,4 @@ +from flask import Module + + +mod = Module(__name__, 'foo', subdomain='foo') diff --git a/tests/subdomaintestmodule/static/hello.txt b/tests/subdomaintestmodule/static/hello.txt new file mode 100644 index 00000000..12e23c16 --- /dev/null +++ b/tests/subdomaintestmodule/static/hello.txt @@ -0,0 +1 @@ +Hello Subdomain From 6e3dd9b3ce3ddb6ae1689654ce6dc2e71742c8a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 20 Aug 2010 11:20:09 +0200 Subject: [PATCH 0488/3747] Added a changelog entry for #108 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index c20a3304..b33d629d 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,8 @@ Bugfix release, release date to be announced. - Jinja2 template loading syntax now allows "./" in front of a template load path. Previously this caused issues with module setups. +- Fixed an issue where the subdomain setting for modules was + ignored for the static folder. Version 0.6 ----------- From c62422c3181f2f8332893b06ee83581d63bceac8 Mon Sep 17 00:00:00 2001 From: agentultra Date: Fri, 10 Sep 2010 03:33:20 +0800 Subject: [PATCH 0489/3747] Updated documentation on packaging patterns --- docs/patterns/packages.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index cd2deac1..35033b59 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -212,6 +212,7 @@ static files and templates. Imagine you have an application like this:: /yourapplication __init__.py /apps + __init__.py /frontend __init__.py views.py @@ -244,6 +245,21 @@ name of the module. So for the admin it would be possible to refer to templates without the prefixed module name. This is explicit unlike URL rules. +You also need to explicitly pass the ``url_prefix`` argument when +registering your modules this way:: + + # in yourapplication/__init__.py + from flask import Flask + from yourapplication.apps.admin.views import admin + from yourapplication.apps.frontend.views import frontend + + + app = Flask(__name__) + app.register_module(admin, url_prefix='/admin') + app.register_module(frontend, url_prefix='/frontend') + +This is because Flask cannot infer the prefix from the package names. + .. admonition:: References to Static Folders Please keep in mind that if you are using unqualified endpoints by From eb67242e1cb08566a1cd2489933479368a48b4a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 10 Sep 2010 11:01:58 -0700 Subject: [PATCH 0490/3747] Fixed a typo in a docstring in app.py --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index f32e4dd8..964394a6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -238,7 +238,7 @@ class Flask(_PackageBoundObject): #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the #: currently logged in user. To register a function here, use the - #: :meth:`before_request` decorator. + #: :meth:`after_request` decorator. self.after_request_funcs = {} #: A dictionary with list of functions that are called without argument From 1e4e578d73b78e2e11696b2e2d0763fd00367521 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 12 Sep 2010 13:18:08 -0700 Subject: [PATCH 0491/3747] Added the extensions dictionary on the application --- flask/app.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/flask/app.py b/flask/app.py index 964394a6..dbb64646 100644 --- a/flask/app.py +++ b/flask/app.py @@ -256,6 +256,22 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.5 self.modules = {} + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. For backwards compatibility extensions should register + #: themselves like this:: + #: + #: if not hasattr(app, 'extensions'): + #: app.extensions = {} + #: app.extensions['extensionname'] = SomeObject() + #: + #: The key must match the name of the `flaskext` module. For example in + #: case of a "Flask-Foo" extension in `flaskext.foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions = {} + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: From 216478f715b330e7d9298c6a11d3e5c0a1e790f8 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 09:47:48 +0800 Subject: [PATCH 0492/3747] docs: Finished sentence on Notes on Proxies. --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ed2a5037..00b5105e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -421,7 +421,7 @@ Notes On Proxies ---------------- Some of the objects provided by Flask are proxies to other objects. The -reason behind this is, that these proxies are shared between threads and +reason behind this is that these proxies are shared between threads and they have to dispatch to the actual object bound to a thread behind the scenes as necessary. @@ -430,7 +430,7 @@ exceptions where it is good to know that this object is an actual proxy: - The proxy objects do not fake their inherited types, so if you want to perform actual instance checks, you have to do that on the instance - that + that is being proxied (see `_get_current_object` below). - if the object reference is important (so for example for sending :ref:`signals`) From 6875a057ec97fb17927b951cd1e5baeef2cf81d8 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 14:05:35 +0800 Subject: [PATCH 0493/3747] Fixed small typos in docs. Added a cross-ref. --- docs/becomingbig.rst | 2 +- docs/config.rst | 4 ++-- docs/deploying/cgi.rst | 2 +- docs/deploying/fastcgi.rst | 2 +- docs/deploying/mod_wsgi.rst | 4 ++-- docs/deploying/others.rst | 4 ++-- docs/extensiondev.rst | 2 +- docs/license.rst | 2 +- docs/patterns/errorpages.rst | 2 +- docs/patterns/fileuploads.rst | 6 +++--- docs/patterns/flashing.rst | 4 ++-- docs/patterns/lazyloading.rst | 2 +- docs/patterns/mongokit.rst | 2 +- docs/patterns/sqlite3.rst | 2 +- docs/patterns/viewdecorators.rst | 2 +- docs/styleguide.rst | 6 +++--- docs/templating.rst | 2 +- docs/tutorial/setup.rst | 2 +- docs/upgrading.rst | 2 +- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 6c95c6e2..20a0186e 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -10,7 +10,7 @@ application there are ways to deal with that. Flask is powered by Werkzeug and Jinja2, two libraries that are in use at a number of large websites out there and all Flask does is bring those two together. Being a microframework Flask does not do much more than -combinding existing libraries - there is not a lot of code involved. +combining existing libraries - there is not a lot of code involved. What that means for large applications is that it's very easy to take the code from Flask and put it into a new module within the applications and expand on that. diff --git a/docs/config.rst b/docs/config.rst index e782bc7f..1c2648a5 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -61,7 +61,7 @@ The following configuration values are used internally by Flask: ``USE_X_SENDFILE`` enable/disable x-sendfile ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name of the server. Required for - subdomain support (eg: ``'localhost'``) + subdomain support (e.g.: ``'localhost'``) ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by @@ -222,7 +222,7 @@ your configuration files. However here a list of good recommendations: even create your own script for sourcing that activates a virtualenv and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and - configurations sepearately to the production server(s). For some + configurations separately to the production server(s). For some details about how to do that, head over to the :ref:`deploy` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index c0b8c560..5d5b085c 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -38,7 +38,7 @@ Server Setup ------------ Usually there are two ways to configure the server. Either just copy the -`.cgi` into a `cgi-bin` (and use `mod_rerwite` or something similar to +`.cgi` into a `cgi-bin` (and use `mod_rewrite` or something similar to rewrite the URL) or let the server point to the file directly. In Apache for example you can put a like like this into the config: diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index a29664e8..19bd42ec 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -123,7 +123,7 @@ webserver user is `www-data`:: $ cd /var/www/yourapplication $ python application.fcgi Traceback (most recent call last): - File "yourapplication.fcg", line 4, in + File "yourapplication.fcgi", line 4, in ImportError: No module named yourapplication In this case the error seems to be "yourapplication" not being on the python diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 5b19f1d5..40df522d 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -93,8 +93,8 @@ For more information consult the `mod_wsgi wiki`_. .. _virtual python: http://pypi.python.org/pypi/virtualenv .. _mod_wsgi wiki: http://code.google.com/p/modwsgi/wiki/ -Toubleshooting --------------- +Troubleshooting +--------------- If your application does not run, follow this guide to troubleshoot: diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 793a4bed..8f08ecd1 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -69,10 +69,10 @@ If you deploy your application behind an HTTP proxy you will need to rewrite a few headers in order for the application to work. The two problematic values in the WSGI environment usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that will solve some common setups, -but you might want to write your own WSGI middlware for specific setups. +but you might want to write your own WSGI middleware for specific setups. The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forwared-For`:: +and the remote address from `X-Forward-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index cfad85f0..1848ca8f 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -153,7 +153,7 @@ There are two recommended ways for an extension to initialize: initialization functions: If your extension is called `helloworld` you might have a function - called ``init_helloworld(app[, extra_args])`` that initalizes the + called ``init_helloworld(app[, extra_args])`` that initializes the extension for that application. It could attach before / after handlers etc. diff --git a/docs/license.rst b/docs/license.rst index 62e5c75e..38777e66 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -4,7 +4,7 @@ License Flask is licensed under a three clause BSD License. It basically means: do whatever you want with it as long as the copyright in Flask sticks around, the conditions are not modified and the disclaimer is present. -Furthermore you must not use the names of the authors to promote derivates +Furthermore you must not use the names of the authors to promote derivatives of the software without written consent. The full license text can be found below (:ref:`flask-license`). For the diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index d7852549..4041bd8a 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -33,7 +33,7 @@ even if the application behaves correctly: instead of 404. If you are not deleting documents permanently from the database but just mark them as deleted, do the user a favour and use the 410 code instead and display a message that what he was - looking for was deleted for all ethernity. + looking for was deleted for all eternity. *500 Internal Server Error* Usually happens on programming errors or if the server is overloaded. diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 99f009c7..221ce327 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -41,9 +41,9 @@ the URL to these files. Why do we limit the extensions that are allowed? You probably don't want your users to be able to upload everything there if the server is directly sending out the data to the client. That way you can make sure that users -are not able to upload HTML files that would cause XSS problems. Also -make sure to disallow `.php` files if the server executes them, but who -has PHP installed on his server, right? :) +are not able to upload HTML files that would cause XSS problems (see +:ref:`xss`). Also make sure to disallow `.php` files if the server +executes them, but who has PHP installed on his server, right? :) Next the functions that check if an extension is valid and that uploads the file and redirects the user to the URL for the uploaded file:: diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 4cea0206..3610944e 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -30,7 +30,7 @@ So here is a full example:: request.form['password'] != 'secret': error = 'Invalid credentials' else: - flash('You were sucessfully logged in') + flash('You were successfully logged in') return redirect(url_for('index')) return render_template('login.html', error=error) @@ -100,7 +100,7 @@ to the :func:`~flask.flash` function:: Inside the template you then have to tell the :func:`~flask.get_flashed_messages` function to also return the -categories. The loop looks slighty different in that situation then: +categories. The loop looks slightly different in that situation then: .. sourcecode:: html+jinja diff --git a/docs/patterns/lazyloading.rst b/docs/patterns/lazyloading.rst index 03b293d8..50ad6fa8 100644 --- a/docs/patterns/lazyloading.rst +++ b/docs/patterns/lazyloading.rst @@ -100,5 +100,5 @@ name and a dot, and by wrapping `view_func` in a `LazyView` as needed:: url('/user/', 'views.user') One thing to keep in mind is that before and after request handlers have -to be in a file that is imported upfront to work propery on the first +to be in a file that is imported upfront to work properly on the first request. The same goes for any kind of remaining decorator. diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index c1727c80..a9c4eef5 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -78,7 +78,7 @@ validator for the maximum character length and uses a special MongoKit feature called `use_dot_notation`. Per default MongoKit behaves like a python dictionary but with `use_dot_notation` set to `True` you can use your documents like you use models in nearly any other ORM by using dots to -seperate between attributes. +separate between attributes. You can insert entries into the database like this: diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 1032097e..68833234 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -61,7 +61,7 @@ Or if you just want a single result:: To pass variable parts to the SQL statement, use a question mark in the statement and pass in the arguments as a list. Never directly add them to -the SQL statement with string formattings because this makes it possible +the SQL statement with string formatting because this makes it possible to attack the application using `SQL Injections `_. diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 73d67852..c61f1a4b 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -89,7 +89,7 @@ Here the code:: return decorated_function return decorator -Notice that this assumes an instanciated `cache` object is available, see +Notice that this assumes an instantiated `cache` object is available, see :ref:`caching-pattern` for more information. diff --git a/docs/styleguide.rst b/docs/styleguide.rst index ec699052..0fdc88d8 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -72,7 +72,7 @@ Expressions and Statements General whitespace rules: - No whitespace for unary operators that are not words - (eg: ``-``, ``~`` etc.) as well on the inner side of parentheses. + (e.g.: ``-``, ``~`` etc.) as well on the inner side of parentheses. - Whitespace is placed between binary operators. Good:: @@ -151,7 +151,7 @@ Docstrings Docstring conventions: All docstrings are formatted with reStructuredText as understood by Sphinx. Depending on the number of lines in the docstring, they are - layed out differently. If it's just one line, the closing triple + laid out differently. If it's just one line, the closing triple quote is on the same line as the opening, otherwise the text is on the same line as the opening quote and the triple quote that closes the string on its own line:: @@ -162,7 +162,7 @@ Docstring conventions: def bar(): """This is a longer docstring with so much information in there - that it spans three lines. In this case the closing tripple quote + that it spans three lines. In this case the closing triple quote is on its own line. """ diff --git a/docs/templating.rst b/docs/templating.rst index 2583cc2c..bd940b0e 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -144,7 +144,7 @@ autoescape %}`` block:

    {{ will_not_be_escaped }} {% endautoescape %} -Whenever you do this, please be very cautious about the varibles you are +Whenever you do this, please be very cautious about the variables you are using in this block. Registering Filters diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index b5ae2a0e..9f762c84 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -76,7 +76,7 @@ focus on that a little later. First we should get the database working. .. admonition:: Externally Visible Server - Want your server to be publically available? Check out the + Want your server to be publicly available? Check out the :ref:`externally visible server ` section for more information. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 91da2b78..17523290 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -52,7 +52,7 @@ order of after-request handlers. Previously they were called in the order of the registration, now they are called in reverse order. This change was made so that Flask behaves more like people expected it to work and how other systems handle request pre- and postprocessing. If you -dependend on the order of execution of post-request functions, be sure to +depend on the order of execution of post-request functions, be sure to change the order. Another change that breaks backwards compatibility is that context From 085faf2a776020c9422bf57b5e37b05705d6b95f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 6 Oct 2010 14:06:46 +0800 Subject: [PATCH 0494/3747] First pass to reword security doc for word flow. --- docs/security.rst | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/security.rst b/docs/security.rst index f3193d62..24a4ceb2 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -62,33 +62,33 @@ Another big problem is CSRF. This is a very complex topic and I won't outline it here in detail just mention what it is and how to theoretically prevent it. -So if your authentication information is stored in cookies you have -implicit state management. By that I mean that the state of "being logged -in" is controlled by a cookie and that cookie is sent with each request to -a page. Unfortunately that really means "each request" so also requests -triggered by 3rd party sites. If you don't keep that in mind some people -might be able to trick your application's users with social engineering to -do stupid things without them knowing. +If your authentication information is stored in cookies, you have implicit +state management. The state of "being logged in" is controlled by a +cookie, and that cookie is sent with each request to a page. +Unfortunately that includes requests triggered by 3rd party sites. If you +don't keep that in mind, some people might be able to trick your +application's users with social engineering to do stupid things without +them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an attacker now creates a page that sends a post request to that page with -some JavaScript he just has to trick some users to that page and their -profiles will end up being deleted. +some JavaScript he just has to trick some users to load that page and +their profiles will end up being deleted. -Imagine you would run Facebook with millions of concurrent users and -someone would send out links to images of little kittens. When a user -would go to that page their profiles would get deleted while they are +Imagine you were to run Facebook with millions of concurrent users and +someone would send out links to images of little kittens. When users +would go to that page, their profiles would get deleted while they are looking at images of fluffy cats. -So how can you prevent yourself from that? Basically for each request -that modifies content on the server you would have to either use a -one-time token and store that in the cookie **and** also transmit it with -the form data. After recieving the data on the server again you would -then have to compare the two tokens and ensure they are equal. +How can you prevent that? Basically for each request that modifies +content on the server you would have to either use a one-time token and +store that in the cookie **and** also transmit it with the form data. +After receiving the data on the server again, you would then have to +compare the two tokens and ensure they are equal. -Why does not Flask do that for you? The ideal place for this to happen is -the form validation framework which does not exist in Flask. +Why does Flask not do that for you? The ideal place for this to happen is +the form validation framework, which does not exist in Flask. .. _json-security: @@ -111,8 +111,8 @@ generate JSON. So what is the issue and how to avoid it? The problem are arrays at toplevel in JSON. Imagine you send the following data out in a JSON -request. Say that's exporting the names and email adresses of all your -friends for a part of the userinterface that is written in JavaScript. +request. Say that's exporting the names and email addresses of all your +friends for a part of the user interface that is written in JavaScript. Not very uncommon: .. sourcecode:: javascript From 325b96099a7221d54b57429d505aaae4d9f67400 Mon Sep 17 00:00:00 2001 From: agentultra Date: Wed, 6 Oct 2010 21:55:42 +0800 Subject: [PATCH 0495/3747] Small change to the packages documentation example for clarity --- docs/patterns/packages.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 35033b59..4ec09e6b 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -162,9 +162,14 @@ modules in the application (`__init__.py`) like this:: from yourapplication.views.frontend import frontend app = Flask(__name__) - app.register_module(admin) + app.register_module(admin, url_prefix='/admin') app.register_module(frontend) +We register the modules with the app so that it can add them to the +URL map for our application. Note the prefix argument to the admin +module: by default when we register a module, that module's end-points +will be registered on `/` unless we specify this argument. + So what is different when working with modules? It mainly affects URL generation. Remember the :func:`~flask.url_for` function? When not working with modules it accepts the name of the function as first From 1d2a308c202f401446fa1f092fe0af904ac0230d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 19 Oct 2010 09:09:55 +0200 Subject: [PATCH 0496/3747] merged --- CHANGES | 6 ++++++ tests/flaskext_test.py | 1 + 2 files changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index b33d629d..358be659 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,12 @@ Release date to be announced, codename to be selected deprecated for :func:`flask.send_file` because it was unreliable. Pass filenames instead or attach your own etags and provide a proper mimetype by hand. +- Static file handling for modules now requires the name of the + static folder to be supplied explicitly. The previous autodetection + was not reliable and caused issues on Google's App Engine. Until + 1.0 the old behaviour will continue to work but issue dependency + warnings. +- fixed a problem for Flask to run on jython. Version 0.6.1 ------------- diff --git a/tests/flaskext_test.py b/tests/flaskext_test.py index 36a62694..d1d5d991 100644 --- a/tests/flaskext_test.py +++ b/tests/flaskext_test.py @@ -201,6 +201,7 @@ envlist=%(env)s [testenv] deps= %(deps)s + distribute py commands=bash flaskext-runtest.sh {envlogdir}/test.log downloadcache=%(cache)s From 405c2992f8de441789a1ed82045ab77cefbbedfc Mon Sep 17 00:00:00 2001 From: Jeff Weber Date: Mon, 18 Oct 2010 23:51:14 -0400 Subject: [PATCH 0497/3747] Updated from_pyfile so its dynamic module creation uses the imp module instead of type. Signed-off-by: Armin Ronacher --- flask/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index a27b362d..aa65f460 100644 --- a/flask/config.py +++ b/flask/config.py @@ -11,6 +11,7 @@ from __future__ import with_statement +import imp import os import sys @@ -114,7 +115,7 @@ class Config(dict): root path. """ filename = os.path.join(self.root_path, filename) - d = type(sys)('config') + d = imp.new_module('config') d.__file__ = filename try: execfile(filename, d.__dict__) From 88883aa6db3b27f132000a4cf2a99c1fbbe941c9 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 21 Oct 2010 20:31:13 +0100 Subject: [PATCH 0498/3747] Fix for Flask's ticket 126. A proper environment is now built to use with `test_request_context()`. Signed-off-by: Armin Ronacher --- flask/app.py | 9 +++++++++ tests/flask_tests.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/flask/app.py b/flask/app.py index dbb64646..2ca641e3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -863,6 +863,15 @@ class Flask(_PackageBoundObject): function accepts the same arguments). """ from werkzeug import create_environ + environ_overrides = kwargs.setdefault('environ_overrides', {}) + if self.config.get('SERVER_NAME'): + server_name = self.config.get('SERVER_NAME') + if ':' not in server_name: + server_name += ':80' + http_host, http_port = server_name.split(':') + environ_overrides.setdefault('SERVER_NAME', server_name) + environ_overrides.setdefault('HTTP_HOST', server_name) + environ_overrides.setdefault('SERVER_PORT', http_port) return self.request_context(create_environ(*args, **kwargs)) def wsgi_app(self, environ, start_response): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 8eecafc0..eac3efca 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -501,6 +501,23 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(repr(flask.g), '') self.assertFalse(flask.g) + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return None + + with app.test_request_context('/'): + assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' + + def testit(): + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + self.assertRaises(ValueError, testit) + class JSONTestCase(unittest.TestCase): From 1f6321a1cbf0e2a40f0b2852ebf763e1160ec7ac Mon Sep 17 00:00:00 2001 From: Nick Walker Date: Mon, 11 Oct 2010 19:07:31 -0700 Subject: [PATCH 0499/3747] clarified description of templates directory Signed-off-by: Armin Ronacher --- docs/tutorial/folders.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst index 5e685c7e..61080932 100644 --- a/docs/tutorial/folders.rst +++ b/docs/tutorial/folders.rst @@ -15,8 +15,8 @@ drop our files. Directly into this folder we will then put our database schema as well as main module in the following steps. The files inside the `static` folder are available to users of the application via `HTTP`. This is the place where css and javascript files go. Inside the -`templates` folder Flask will look for `Jinja2`_ templates. Drop all the -templates there. +`templates` folder Flask will look for `Jinja2`_ templates. The +templates you create later in the tutorial will go in this directory. Continue with :ref:`tutorial-schema`. From a327452540bbcffe73207f8164504115b6c9e856 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Fri, 22 Oct 2010 16:13:35 +0100 Subject: [PATCH 0500/3747] Better handling for `test_request_context` don't just append the port. Also implemented a proper initial environment to use with `Flask.test_app()` based on the application's configuration. Signed-off-by: Armin Ronacher --- flask/app.py | 8 ++-- flask/testing.py | 26 ++++++++++- tests/flask_tests.py | 105 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 7 deletions(-) diff --git a/flask/app.py b/flask/app.py index 2ca641e3..aa6371d6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -704,7 +704,7 @@ class Flask(_PackageBoundObject): raise req.routing_exception rule = req.url_rule # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically + # request came with the OPTIONS method, reply automatically if rule.provide_automatic_options and req.method == 'OPTIONS': return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint @@ -867,8 +867,10 @@ class Flask(_PackageBoundObject): if self.config.get('SERVER_NAME'): server_name = self.config.get('SERVER_NAME') if ':' not in server_name: - server_name += ':80' - http_host, http_port = server_name.split(':') + http_host, http_port = server_name, '80' + else: + http_host, http_port = server_name.split(':', 1) + environ_overrides.setdefault('SERVER_NAME', server_name) environ_overrides.setdefault('HTTP_HOST', server_name) environ_overrides.setdefault('SERVER_PORT', http_port) diff --git a/flask/testing.py b/flask/testing.py index ee4bd28e..84237336 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -from werkzeug import Client +from werkzeug import Client, EnvironBuilder from flask import _request_ctx_stack @@ -29,9 +29,31 @@ class FlaskClient(Client): self.context_preserved = False kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context + + as_tuple = kwargs.pop('as_tuple', False) + buffered = kwargs.pop('buffered', False) + follow_redirects = kwargs.pop('follow_redirects', False) + + builder = EnvironBuilder(*args, **kwargs) + + if self.application.config.get('SERVER_NAME'): + server_name = self.application.config.get('SERVER_NAME') + if ':' not in server_name: + http_host, http_port = server_name, None + else: + http_host, http_port = server_name.split(':', 1) + if builder.base_url == 'http://localhost/': + # Default Generated Base URL + if http_port != None: + builder.host = http_host + ':' + http_port + else: + builder.host = http_host old = _request_ctx_stack.top try: - return Client.open(self, *args, **kwargs) + return Client.open(self, builder, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects) finally: self.context_preserved = _request_ctx_stack.top is not old diff --git a/tests/flask_tests.py b/tests/flask_tests.py index eac3efca..dddabf21 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -510,13 +510,114 @@ class BasicFunctionalityTestCase(unittest.TestCase): def index(): return None + @app.route('/', subdomain='foo') + def sub(): + return None + with app.test_request_context('/'): assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' - def testit(): + with app.test_request_context('/'): + assert flask.url_for('sub', _external=True) == 'http://foo.localhost.localdomain:5000/' + + try: with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): pass - self.assertRaises(ValueError, testit) + except Exception, e: + assert isinstance(e, ValueError) + assert str(e) == "the server name provided " + \ + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')", str(e) + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + + def test_test_app_proper_environ(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return 'Foo' + + @app.route('/', subdomain='foo') + def subdomain(): + return 'Foo SubDomain' + + try: + rv = app.test_client().get('/') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain:443') + rv = app.test_client().get('/', 'https://localhost.localdomain') + assert rv.data == 'Foo' + except ValueError, e: + assert str(e) == "the server name provided " + \ + "('localhost.localdomain:443') does not match the " + \ + "server name from the WSGI environment ('localhost.localdomain')", str(e) + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + app.test_client().get('/', 'http://foo.localhost') + except ValueError, e: + assert str(e) == "the server name provided " + \ + "('localhost.localdomain') does not match the " + \ + "server name from the WSGI environment ('foo.localhost')", str(e) + + try: + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + assert rv.data == 'Foo SubDomain' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) class JSONTestCase(unittest.TestCase): From d4c44a7d8c1554dfafc240f6a1714bc58fdc05f5 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Mon, 25 Oct 2010 15:38:39 +0200 Subject: [PATCH 0501/3747] BaseConverter is in werkzeug.routing Signed-off-by: Armin Ronacher --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index aa6371d6..f2ec45e9 100644 --- a/flask/app.py +++ b/flask/app.py @@ -276,7 +276,7 @@ class Flask(_PackageBoundObject): #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: #: - #: from werkzeug import BaseConverter + #: from werkzeug.routing import BaseConverter #: #: class ListConverter(BaseConverter): #: def to_python(self, value): From 94a7312d5ac5e2f949c4e27ecf2a273cec3d8c0c Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Fri, 29 Oct 2010 10:55:10 +0200 Subject: [PATCH 0502/3747] Add missing semicolon to test Signed-off-by: Armin Ronacher --- docs/testing.rst | 2 +- examples/flaskr/flaskr_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 790eddf6..b9f0806b 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -173,7 +173,7 @@ like this:: text='HTML allowed here' ), follow_redirects=True) assert 'No entries here so far' not in rv.data - assert '<Hello>' in rv.data + assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data Here we check that HTML is allowed in the text but not in the title, diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index 07e702c4..06bf1035 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -66,7 +66,7 @@ class FlaskrTestCase(unittest.TestCase): text='HTML allowed here' ), follow_redirects=True) assert 'No entries here so far' not in rv.data - assert '<Hello>' in rv.data + assert '<Hello>' in rv.data assert 'HTML allowed here' in rv.data From d8f521e1ff200918e2bdeb526e535252763e653a Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Thu, 28 Oct 2010 22:41:27 +0200 Subject: [PATCH 0503/3747] Add missing period Signed-off-by: Armin Ronacher --- docs/tutorial/setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 9f762c84..64bf3b66 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -56,7 +56,7 @@ execute code on the server! We also add a method to easily connect to the database specified. That can be used to open a connection on request and also from the interactive -Python shell or a script. This will come in handy later +Python shell or a script. This will come in handy later. :: From ce4f81f1c235dad3667885398794112690fa0462 Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Thu, 28 Oct 2010 22:30:07 +0200 Subject: [PATCH 0504/3747] Correct capitalization SQLite Signed-off-by: Armin Ronacher --- docs/tutorial/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index ff7cdcab..00fe3a00 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -14,7 +14,7 @@ less web-2.0-ish name ;) Basically we want it to do the following things: 3. the page shows all entries so far in reverse order (newest on top) and the user can add new ones from there if logged in. -We will be using SQlite3 directly for that application because it's good +We will be using SQLite3 directly for that application because it's good enough for an application of that size. For larger applications however it makes a lot of sense to use `SQLAlchemy`_ that handles database connections in a more intelligent way, allows you to target different From 693b3b7621f7bf1a1aa5eaae90fc48179c0d3088 Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Thu, 28 Oct 2010 22:22:17 +0200 Subject: [PATCH 0505/3747] Tricker -> trickier Signed-off-by: Armin Ronacher --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index a9dc5c28..eb645bdc 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -147,7 +147,7 @@ To just get the development version without git, do this instead:: `easy_install` on Windows ------------------------- -On Windows, installation of `easy_install` is a little bit tricker because +On Windows, installation of `easy_install` is a little bit trickier because slightly different rules apply on Windows than on Unix-like systems, but it's not difficult. The easiest way to do it is to download the `ez_setup.py`_ file and run it. The easiest way to run the file is to From 60177639f4ec207b43e94cd59c5bad1a7a67938b Mon Sep 17 00:00:00 2001 From: Daniel Haaker Date: Thu, 28 Oct 2010 22:10:50 +0200 Subject: [PATCH 0506/3747] There is no database abstraction layer Signed-off-by: Armin Ronacher --- docs/foreword.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 308de9c5..bdb6a10d 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -32,7 +32,7 @@ static files are in subdirectories within the Python source tree of the application. The main reason however why Flask is called a "microframework" is the idea -to keep the core simple but extensible. There is database abstraction +to keep the core simple but extensible. There is no database abstraction layer, no form validation or anything else where different libraries already exist that can handle that. However Flask knows the concept of extensions that can add this functionality into your application as if it From 1c24b62727704016813a44d7c677ead0b6638c91 Mon Sep 17 00:00:00 2001 From: Francisco Souza Date: Sun, 31 Oct 2010 01:55:06 -0200 Subject: [PATCH 0507/3747] Minor on docs (fixes #133) Signed-off-by: Armin Ronacher --- docs/signals.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 73a0c744..356bfcb2 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -179,8 +179,8 @@ The following signals exist in Flask: template.name or 'string template', context) - from flask import request_started - request_started.connect(log_template_renders, app) + from flask import template_rendered + template_rendered.connect(log_template_renders, app) .. data:: flask.request_started :noindex: From 4f1b086218437870c0d36b4b9b36c5376cad9206 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Sat, 30 Oct 2010 20:55:41 -0400 Subject: [PATCH 0508/3747] Fixed: incorrect indentation level for session's permanent attribute Signed-off-by: Armin Ronacher --- docs/api.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 00b5105e..5de90d01 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -187,12 +187,12 @@ To access the current session you can use the :class:`session` object: # so mark it as modified yourself session.modified = True - .. attribute:: permanent + .. attribute:: permanent - If set to `True` the session life for - :attr:`~flask.Flask.permanent_session_lifetime` seconds. The - default is 31 days. If set to `False` (which is the default) the - session will be deleted when the user closes the browser. + If set to `True` the session life for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to `False` (which is the default) the + session will be deleted when the user closes the browser. Application Globals From 67f483bf647549d6f3a6a3b5f73a2bf913a0b25c Mon Sep 17 00:00:00 2001 From: doobeh Date: Wed, 3 Nov 2010 12:10:44 -0700 Subject: [PATCH 0509/3747] Typo. Signed-off-by: Armin Ronacher --- docs/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/security.rst b/docs/security.rst index 24a4ceb2..a8a0afcf 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -11,7 +11,7 @@ Cross-Site Scripting (XSS) -------------------------- Cross site scripting is the concept of injecting arbitrary HTML (and with -it JavaScript) into the context of a website. To rememdy this, developers +it JavaScript) into the context of a website. To remedy this, developers have to properly escape text so that it cannot include arbitrary HTML tags. For more information on that have a look at the Wikipedia article on `Cross-Site Scripting From 858806da69f9f46eb4abaf67bf78d864ea104760 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Mon, 8 Nov 2010 18:02:39 +0100 Subject: [PATCH 0510/3747] Hint for positioning file pointer correctly before calling send_file(). Signed-off-by: Armin Ronacher --- flask/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 18eb6d0e..33aa7ee2 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -287,7 +287,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, relative path is specified. Alternatively a file object might be provided in which case `X-Sendfile` might not work and - fall back to the traditional method. + fall back to the traditional method. Make sure + that the file pointer is positioned at the start + of data to send before calling :func:`send_file`. :param mimetype: the mimetype of the file if provided, otherwise auto detection happens. :param as_attachment: set to `True` if you want to send this file with From 2ac1b7d4388209d8e1309ed5b43ebe8d023eaa53 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Nov 2010 13:25:45 +0100 Subject: [PATCH 0511/3747] Fixed a documentation error. This fixes #143 --- docs/signals.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 356bfcb2..ed5ecd51 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -81,16 +81,23 @@ context are appended to it. Additionally there is a convenient helper method (:meth:`~blinker.base.Signal.connected_to`). that allows you to temporarily subscribe a function to a signal with is a context manager on -its own which simplifies the example above:: +its own. Because the return value of the context manager cannot be +specified that way one has to pass the list in as argument:: from flask import template_rendered - def captured_templates(app): - recorded = [] + def captured_templates(app, recorded): def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) +The example above would then look like this:: + + templates = [] + with captured_templates(app, templates): + ... + template, context = templates[0] + .. admonition:: Blinker API Changes The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker From ed517c7215fc17a46f85c2c0324d519572b67f6b Mon Sep 17 00:00:00 2001 From: Jason Davies Date: Sat, 13 Nov 2010 11:28:42 +0000 Subject: [PATCH 0512/3747] Minor spelling fixes Signed-off-by: Armin Ronacher --- CHANGES | 4 ++-- docs/foreword.rst | 2 +- docs/unicode.rst | 28 ++++++++++++++-------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGES b/CHANGES index 358be659..1a452f40 100644 --- a/CHANGES +++ b/CHANGES @@ -45,7 +45,7 @@ Released on July 27th 2010, codename Whisky - after request functions are now called in reverse order of registration. - OPTIONS is now automatically implemented by Flask unless the - application explictly adds 'OPTIONS' as method to the URL rule. + application explicitly adds 'OPTIONS' as method to the URL rule. In this case no automatic OPTIONS handling kicks in. - static rules are now even in place if there is no static folder for the module. This was implemented to aid GAE which will @@ -65,7 +65,7 @@ Released on July 27th 2010, codename Whisky - added signalling support based on blinker. This feature is currently optional and supposed to be used by extensions and applications. If you want to use it, make sure to have `blinker`_ installed. -- refactored the way url adapters are created. This process is now +- refactored the way URL adapters are created. This process is now fully customizable with the :meth:`~flask.Flask.create_url_adapter` method. - modules can now register for a subdomain instead of just an URL diff --git a/docs/foreword.rst b/docs/foreword.rst index bdb6a10d..2c073218 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -106,4 +106,4 @@ Werkzeug and Flask will be ported to Python 3 as soon as a solution for WSGI is found, and we will provide helpful tips how to upgrade existing applications to Python 3. Until then, we strongly recommend using Python 2.6 and 2.7 with activated Python 3 warnings during development, as well -as the unicode literals `__future__` feature. +as the Unicode literals `__future__` feature. diff --git a/docs/unicode.rst b/docs/unicode.rst index 5b4202ad..413ea84d 100644 --- a/docs/unicode.rst +++ b/docs/unicode.rst @@ -1,23 +1,23 @@ Unicode in Flask ================ -Flask like Jinja2 and Werkzeug is totally unicode based when it comes to +Flask like Jinja2 and Werkzeug is totally Unicode based when it comes to text. Not only these libraries, also the majority of web related Python -libraries that deal with text. If you don't know unicode so far, you +libraries that deal with text. If you don't know Unicode so far, you should probably read `The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets `_. This part of the documentation just tries to cover the very basics so that you have a -pleasant experience with unicode related things. +pleasant experience with Unicode related things. Automatic Conversion -------------------- Flask has a few assumptions about your application (which you can change -of course) that give you basic and painless unicode support: +of course) that give you basic and painless Unicode support: - the encoding for text on your website is UTF-8 -- internally you will always use unicode exclusively for text except +- internally you will always use Unicode exclusively for text except for literal strings with only ASCII character points. - encoding and decoding happens whenever you are talking over a protocol that requires bytes to be transmitted. @@ -29,27 +29,27 @@ address documents on servers (so called URIs or URLs). However HTML which is usually transmitted on top of HTTP supports a large variety of character sets and which ones are used, are transmitted in an HTTP header. To not make this too complex Flask just assumes that if you are sending -unicode out you want it to be UTF-8 encoded. Flask will do the encoding +Unicode out you want it to be UTF-8 encoded. Flask will do the encoding and setting of the appropriate headers for you. The same is true if you are talking to databases with the help of SQLAlchemy or a similar ORM system. Some databases have a protocol that -already transmits unicode and if they do not, SQLAlchemy or your other ORM +already transmits Unicode and if they do not, SQLAlchemy or your other ORM should take care of that. The Golden Rule --------------- So the rule of thumb: if you are not dealing with binary data, work with -unicode. What does working with unicode in Python 2.x mean? +Unicode. What does working with Unicode in Python 2.x mean? - as long as you are using ASCII charpoints only (basically numbers, some special characters of latin letters without umlauts or anything fancy) you can use regular string literals (``'Hello World'``). - if you need anything else than ASCII in a string you have to mark - this string as unicode string by prefixing it with a lowercase `u`. + this string as Unicode string by prefixing it with a lowercase `u`. (like ``u'Hänsel und Gretel'``) -- if you are using non-unicode characters in your Python files you have +- if you are using non-Unicode characters in your Python files you have to tell Python which encoding your file uses. Again, I recommend UTF-8 for this purpose. To tell the interpreter your encoding you can put the ``# -*- coding: utf-8 -*-`` into the first or second line of @@ -61,21 +61,21 @@ Encoding and Decoding Yourself ------------------------------ If you are talking with a filesystem or something that is not really based -on unicode you will have to ensure that you decode properly when working -with unicode interface. So for example if you want to load a file on the +on Unicode you will have to ensure that you decode properly when working +with Unicode interface. So for example if you want to load a file on the filesystem and embed it into a Jinja2 template you will have to decode it from the encoding of that file. Here the old problem that text files do not specify their encoding comes into play. So do yourself a favour and limit yourself to UTF-8 for text files as well. -Anyways. To load such a file with unicode you can use the built-in +Anyways. To load such a file with Unicode you can use the built-in :meth:`str.decode` method:: def read_file(filename, charset='utf-8'): with open(filename, 'r') as f: return f.read().decode(charset) -To go from unicode into a specific charset such as UTF-8 you can use the +To go from Unicode into a specific charset such as UTF-8 you can use the :meth:`unicode.encode` method:: def write_file(filename, contents, charset='utf-8'): From 8569dfee6136bfda4a5fa2297aed3d82700bb1a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 29 Nov 2010 08:57:38 +0100 Subject: [PATCH 0513/3747] Added a PROPAGATE_EXCEPTIONS flag --- CHANGES | 4 ++++ docs/config.rst | 8 ++++++++ flask/app.py | 15 ++++++++++++++- tests/flask_tests.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 1a452f40..7d9f9d25 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,10 @@ Release date to be announced, codename to be selected 1.0 the old behaviour will continue to work but issue dependency warnings. - fixed a problem for Flask to run on jython. +- added a `PROPAGATE_EXCEPTIONS` configuration variable that can be + used to flip the setting of exception propagation which previously + was linked to `DEBUG` alone and is now linked to either `DEBUG` or + `TESTING`. Version 0.6.1 ------------- diff --git a/docs/config.rst b/docs/config.rst index 1c2648a5..de74aa2b 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -54,6 +54,11 @@ The following configuration values are used internally by Flask: =============================== ========================================= ``DEBUG`` enable/disable debug mode ``TESTING`` enable/disable testing mode +``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the + propagation of exceptions. If not set or + explicitly set to `None` this is + implicitly true if either `TESTING` or + `DEBUG` is true. ``SECRET_KEY`` the secret key ``SESSION_COOKIE_NAME`` the name of the session cookie ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as @@ -96,6 +101,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.6 ``MAX_CONTENT_LENGTH`` +.. versionadded:: 0.7 + ``PROPAGATE_EXCEPTIONS`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index f2ec45e9..312291e1 100644 --- a/flask/app.py +++ b/flask/app.py @@ -189,6 +189,7 @@ class Flask(_PackageBoundObject): default_config = ImmutableDict({ 'DEBUG': False, 'TESTING': False, + 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), @@ -303,6 +304,18 @@ class Flask(_PackageBoundObject): self.jinja_env = self.create_jinja_environment() self.init_jinja_globals() + @property + def propagate_exceptions(self): + """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration + value in case it's set, otherwise a sensible default is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PROPAGATE_EXCEPTIONS'] + if rv is not None: + return rv + return self.testing or self.debug + @property def logger(self): """A :class:`logging.Logger` object for this application. The @@ -682,7 +695,7 @@ class Flask(_PackageBoundObject): """ got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) - if self.debug: + if self.propagate_exceptions: raise self.logger.exception('Exception on %s [%s]' % ( request.path, diff --git a/tests/flask_tests.py b/tests/flask_tests.py index dddabf21..c1cb95c2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -15,6 +15,7 @@ import re import sys import flask import unittest +from threading import Thread from logging import StreamHandler from contextlib import contextmanager from datetime import datetime @@ -547,7 +548,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): "No ValueError exception should have been raised \"%s\"" % e ) - def test_test_app_proper_environ(self): app = flask.Flask(__name__) app.config.update( @@ -619,6 +619,33 @@ class BasicFunctionalityTestCase(unittest.TestCase): "No ValueError exception should have been raised \"%s\"" % e ) + def test_exception_propagation(self): + def apprunner(configkey): + app = flask.Flask(__name__) + @app.route('/') + def index(): + 1/0 + c = app.test_client() + if config_key is not None: + app.config[config_key] = True + try: + resp = c.get('/') + except Exception: + pass + else: + self.fail('expected exception') + else: + assert c.get('/').status_code == 500 + + # we have to run this test in an isolated thread because if the + # debug flag is set to true and an exception happens the context is + # not torn down. This causes other tests that run after this fail + # when they expect no exception on the stack. + for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: + t = Thread(target=apprunner, args=(config_key,)) + t.start() + t.join() + class JSONTestCase(unittest.TestCase): From 164067920ba7832e587a24725384789ab92bd55f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 1 Dec 2010 17:22:55 +0100 Subject: [PATCH 0514/3747] Updated examples to work with pypy which has a incomplete sqlite3 in 1.4. Also disable a euc-kr test that does not work on pypy --- examples/flaskr/flaskr.py | 2 +- examples/minitwit/minitwit.py | 2 +- tests/flask_tests.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 1df24293..69953555 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ from __future__ import with_statement -import sqlite3 +from sqlite3 import dbapi2 as sqlite3 from contextlib import closing from flask import Flask, request, session, g, redirect, url_for, abort, \ render_template, flash diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index b740bc25..7726e9f4 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -10,7 +10,7 @@ """ from __future__ import with_statement import time -import sqlite3 +from sqlite3 import dbapi2 as sqlite3 from hashlib import md5 from datetime import datetime from contextlib import closing diff --git a/tests/flask_tests.py b/tests/flask_tests.py index c1cb95c2..9e68a7a3 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -29,6 +29,15 @@ sys.path.append(os.path.join(example_path, 'flaskr')) sys.path.append(os.path.join(example_path, 'minitwit')) +def has_encoding(name): + try: + import codecs + codecs.lookup(name) + return True + except LookupError: + return False + + # config keys used for the ConfigTestCase TEST_KEY = 'foo' SECRET_KEY = 'devkey' @@ -698,6 +707,9 @@ class JSONTestCase(unittest.TestCase): assert rv.status_code == 200 assert rv.data == u'정상처리'.encode('utf-8') + if not has_encoding('euc-kr'): + test_modified_url_encoding = None + class TemplatingTestCase(unittest.TestCase): From 4b65c7ed5a31d56e194817231eb88e1083ba285d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 6 Dec 2010 03:16:13 +0100 Subject: [PATCH 0515/3747] Added the ability to override the test client's class. This fixes #148 --- flask/app.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index 312291e1..3d344abb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -199,6 +199,11 @@ class Flask(_PackageBoundObject): 'MAX_CONTENT_LENGTH': None }) + #: the test client that is used with when `test_client` is used. + #: + #: .. versionadded:: 0.7 + test_client_class = None + def __init__(self, import_name, static_path=None): _PackageBoundObject.__init__(self, import_name) if static_path is not None: @@ -429,7 +434,7 @@ class Flask(_PackageBoundObject): options.setdefault('use_debugger', self.debug) return run_simple(host, port, self, **options) - def test_client(self): + def test_client(self, use_cookies=True): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. @@ -443,9 +448,16 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.4 added support for `with` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. """ - from flask.testing import FlaskClient - return FlaskClient(self, self.response_class, use_cookies=True) + cls = self.test_client_class + if cls is None: + from flask.testing import FlaskClient as cls + return cls(self, self.response_class, use_cookies=use_cookies) def open_session(self, request): """Creates or opens a new session. Default implementation stores all From fb88d9d0253bffdd67665d2e1b761bd3d9986804 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 7 Dec 2010 22:08:46 +0100 Subject: [PATCH 0516/3747] Whitespace nazi was here --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 3d344abb..7172fccb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -200,7 +200,7 @@ class Flask(_PackageBoundObject): }) #: the test client that is used with when `test_client` is used. - #: + #: #: .. versionadded:: 0.7 test_client_class = None From ed70b42798a31bce951917ff22b996c810e2c3a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 Dec 2010 14:15:18 +0100 Subject: [PATCH 0517/3747] Make sure that windows servers do not allow downloading arbitrary files --- flask/helpers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 33aa7ee2..a783dc12 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -58,6 +58,13 @@ else: _tojson_filter = json.dumps +# what separators does this operating system provide that are not a slash? +# this is used by the send_from_directory function to ensure that nobody is +# able to access files from outside the filesystem. +_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] + if sep not in (None, '/')) + + def _endpoint_from_view_func(view_func): """Internal helper that returns the default endpoint for a given function. This always is the function name. @@ -413,7 +420,10 @@ def send_from_directory(directory, filename, **options): forwarded to :func:`send_file`. """ filename = posixpath.normpath(filename) - if filename.startswith(('/', '../')): + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): raise NotFound() filename = os.path.join(directory, filename) if not os.path.isfile(filename): From b92120b190e92288468d8617bc0e270dbf32ea71 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 Dec 2010 14:18:14 +0100 Subject: [PATCH 0518/3747] Documented security fix in changelog --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 7d9f9d25..e83b2b95 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,9 @@ Bugfix release, release date to be announced. module setups. - Fixed an issue where the subdomain setting for modules was ignored for the static folder. +- Fixed a security problem that allowed clients to download arbitrary files + if the host server was a windows based operating system and the client + uses backslashes to escape the directory the files where exposed from. Version 0.6 ----------- From 11c66be80e3e249c5f14dd9a23f49a073cd03903 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 Dec 2010 14:23:33 +0100 Subject: [PATCH 0519/3747] Added testcase for an issue that may exist on windows --- tests/flask_tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 9e68a7a3..c5700ff8 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -996,6 +996,21 @@ class ModuleTestCase(unittest.TestCase): else: assert 0, 'expected exception' + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + finally: + os.path = old_path + class SendfileTestCase(unittest.TestCase): From 4c76607553e92f6e1b03930e053cc7078fc32f8d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 31 Dec 2010 15:21:46 +0100 Subject: [PATCH 0520/3747] Released 0.6.1 Signed-off-by: Armin Ronacher --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e83b2b95..7d204466 100644 --- a/CHANGES +++ b/CHANGES @@ -31,7 +31,7 @@ Release date to be announced, codename to be selected Version 0.6.1 ------------- -Bugfix release, release date to be announced. +Bugfix release, released on December 31st 2010 - Fixed an issue where the default `OPTIONS` response was not exposing all valid methods in the `Allow` header. From 9ae4eba9861bfa959bac6adfaf5842ef7d937eb6 Mon Sep 17 00:00:00 2001 From: Daniel Gerber Date: Thu, 6 Jan 2011 16:43:49 +0100 Subject: [PATCH 0521/3747] fix subdomain for static rule on registering module Signed-off-by: Armin Ronacher --- flask/module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/module.py b/flask/module.py index 5f3c8c1f..4e719a4e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -32,7 +32,7 @@ def _register_module(module, static_path): state.app.add_url_rule(path + '/', endpoint='%s.static' % module.name, view_func=module.send_static_file, - subdomain=module.subdomain) + subdomain=state.subdomain) return _register From 24c1713ff5f08ee1a2fa0adf9774328b719d3870 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 13 Jan 2011 22:18:49 +0100 Subject: [PATCH 0522/3747] Improved documentation for model registration --- docs/patterns/sqlalchemy.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index a66627ad..97317881 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -47,6 +47,10 @@ Here the example `database.py` module for your application:: Base.query = db_session.query_property() def init_db(): + # import all modules here that might define models so that + # they will be registered properly on the metadata. Otherwise + # you will have to import them first before calling init_db() + import yourapplication.models Base.metadata.create_all(bind=engine) To define your models, just subclass the `Base` class that was created by @@ -84,6 +88,11 @@ Here is an example model (put this into `models.py`, e.g.):: def __repr__(self): return '' % (self.name) +To create the database you can use the `init_db` function: + +>>> from yourapplication.database import init_db +>>> init_db() + You can insert entries into the database like this: >>> from yourapplication.database import db_session From 402f12bb6d578b767dfd637400f9194736458795 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 14 Jan 2011 00:29:37 +0100 Subject: [PATCH 0523/3747] We do not support 2.4, not even with CGI :) --- docs/conf.py | 2 +- docs/deploying/cgi.rst | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3be35fb6..390361a6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,7 +243,7 @@ latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] intersphinx_mapping = { 'http://docs.python.org/dev': None, - 'http://werkzeug.pocoo.org/documentation/dev/': None, + 'http://werkzeug.pocoo.org/docs/': None, 'http://www.sqlalchemy.org/docs/': None, 'http://wtforms.simplecodes.com/docs/0.5/': None, 'http://discorporate.us/projects/Blinker/docs/1.1/': None diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 5d5b085c..830ab28a 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -31,9 +31,6 @@ First you need to create the CGI application file. Let's call it CGIHandler().run(app) -If you're running Python 2.4 you will need the :mod:`wsgiref` package. Python -2.5 and higher ship this as part of the standard library. - Server Setup ------------ From 07688d7b9302fd47cfd0ea053171ad6561c3f9b9 Mon Sep 17 00:00:00 2001 From: Ivo Danihelka Date: Sun, 16 Jan 2011 13:26:34 +0100 Subject: [PATCH 0524/3747] Fixed 'schemal' doc typo. Signed-off-by: Armin Ronacher --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index a783dc12..9d64c198 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -480,7 +480,7 @@ class _PackageBoundObject(object): how this works, consider the following folder structure:: /myapplication.py - /schemal.sql + /schema.sql /static /style.css /templates From 99be2ec022e2ceb1c21527c8278a7179630430d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 16 Jan 2011 17:13:25 +0100 Subject: [PATCH 0525/3747] Flask no longer internally depends on rules being added through the add_url_rule function --- CHANGES | 3 +++ flask/app.py | 3 ++- tests/flask_tests.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7d204466..017eee1f 100644 --- a/CHANGES +++ b/CHANGES @@ -27,6 +27,9 @@ Release date to be announced, codename to be selected used to flip the setting of exception propagation which previously was linked to `DEBUG` alone and is now linked to either `DEBUG` or `TESTING`. +- Flask no longer internally depends on rules being added through the + `add_url_rule` function and can now also accept regular werkzeug + rules added to the url map. Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index 7172fccb..448df8fc 100644 --- a/flask/app.py +++ b/flask/app.py @@ -730,7 +730,8 @@ class Flask(_PackageBoundObject): rule = req.url_rule # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically - if rule.provide_automatic_options and req.method == 'OPTIONS': + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index c5700ff8..e045290a 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -206,6 +206,24 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + def test_werkzeug_routing(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + def bar(): + return 'bar' + def index(): + return 'index' + app.view_functions['bar'] = bar + app.view_functions['index'] = index + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + def test_session(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From 7afd49c6488461a82505654a88861cdb16356b74 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Wed, 12 Jan 2011 10:35:44 +0100 Subject: [PATCH 0526/3747] Pattern documentation for favicons Signed-off-by: Armin Ronacher --- docs/patterns/favicon.rst | 55 +++++++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 2 files changed, 56 insertions(+) create mode 100644 docs/patterns/favicon.rst diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst new file mode 100644 index 00000000..6cf64ea0 --- /dev/null +++ b/docs/patterns/favicon.rst @@ -0,0 +1,55 @@ +.. _favicon: + +Adding a favicon +================ + +A "favicon" is an icon used by browsers for tabs and bookmarks. This helps +to distinguish your website and to give it a unique brand. + +A common question is how to add a favicon to a flask application. First, of +course, you need an icon. It should be 16 × 16 pixels and in the ICO file +format. This is not a requirement but a de-facto standard supported by all +relevant browsers. Put the icon in your static directory as +:file:`favicon.ico`. + +Now, to get browsers to find your icon, the correct way is to add a link +tag in your HTML. So, for example: + +.. sourcecode:: html+jinja + + + +That's all you need for most browsers, however some really old ones do not +support this standard. The old de-facto standard is to serve this file, +with this name, at the website root. If your application is not mounted at +the root path of the domain you either need to configure the webserver to +serve the icon at the root or if you can't do that you're out of luck. If +however your application is the root you can simply route a redirect:: + + app.add_url_rule('/favicon.ico', + redirect_to=url_for('static', filename='favicon.ico')) + +If you want to save the extra redirect request you can also write a view +using :func:`~flask.send_from_directory`:: + + import os + from flask import send_from_directory + + @app.route('/favicon.ico') + def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + +We can leave out the explicit mimetype and it will be guessed, but we may +as well specify it to avoid the extra guessing, as it will always be the +same. + +The above will serve the icon via your application and if possible it's +better to configure your dedicated web server to serve it; refer to the +webserver's documentation. + +See also +-------- + +* The `Favicon `_ article on + Wikipedia diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 21f3a562..68cff143 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -32,3 +32,4 @@ Snippet Archives `_. errorpages lazyloading mongokit + favicon From 4a45345fc44f60275b91c47031dd44abce02f596 Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Wed, 12 Jan 2011 10:38:20 +0100 Subject: [PATCH 0527/3747] Fix typo in send_file docs Signed-off-by: Armin Ronacher --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 9d64c198..eb12eee6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -266,7 +266,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, By default it will try to guess the mimetype for you, but you can also explicitly provide one. For extra security you probably want - to sent certain files as attachment (HTML for instance). The mimetype + to send certain files as attachment (HTML for instance). The mimetype guessing requires a `filename` or an `attachment_filename` to be provided. From 384ad219cbde8ecbea79d41815d9ab2d70e3123c Mon Sep 17 00:00:00 2001 From: Dag Odenhall Date: Mon, 10 Jan 2011 02:13:39 +0100 Subject: [PATCH 0528/3747] Typo in docs Signed-off-by: Armin Ronacher --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 5de90d01..50c9820d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -189,7 +189,7 @@ To access the current session you can use the :class:`session` object: .. attribute:: permanent - If set to `True` the session life for + If set to `True` the session lives for :attr:`~flask.Flask.permanent_session_lifetime` seconds. The default is 31 days. If set to `False` (which is the default) the session will be deleted when the user closes the browser. From 8a73097fe528430eb77ac26c81ba85229a8af553 Mon Sep 17 00:00:00 2001 From: mvantellingen Date: Sun, 23 Jan 2011 19:52:27 +0100 Subject: [PATCH 0529/3747] Add unittests for the endpoint decorator Signed-off-by: Armin Ronacher --- tests/flask_tests.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e045290a..db2616a9 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -224,6 +224,26 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert c.get('/foo/').data == 'index' assert c.get('/foo/bar').data == 'bar' + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + + @app.endpoint('bar') + def bar(): + return 'bar' + + @app.endpoint('index') + def index(): + return 'index' + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + def test_session(self): app = flask.Flask(__name__) app.secret_key = 'testkey' @@ -1029,6 +1049,31 @@ class ModuleTestCase(unittest.TestCase): finally: os.path = old_path + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + from flask import Module + + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + module = Module(__name__, __name__) + + @module.endpoint('bar') + def bar(): + return 'bar' + + @module.endpoint('index') + def index(): + return 'index' + + app.register_module(module) + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + class SendfileTestCase(unittest.TestCase): From b0ca7e5af15ef2218b9726aba1d8948a409dedb9 Mon Sep 17 00:00:00 2001 From: mvantellingen Date: Sun, 23 Jan 2011 13:31:02 +0100 Subject: [PATCH 0530/3747] Implement the endpoint decorator. This allows you to easily map views to endpoints when using the werkzeug routing. Signed-off-by: Armin Ronacher --- docs/patterns/viewdecorators.rst | 21 +++++++++++++++++++++ flask/app.py | 17 +++++++++++++++++ flask/module.py | 8 ++++++++ 3 files changed, 46 insertions(+) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index c61f1a4b..e99fc13e 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -145,3 +145,24 @@ Here the code for that decorator:: return render_template(template_name, **ctx) return decorated_function return decorator + + +Endpoint Decorator +------------------ + +When you want to use the werkzeug routing system for more flexibility you +need to map the endpoint as defined in the :class:`~werkzeug.routing.Rule` +to a view function. This is possible with this decorator. For example:: + + from flask import Flask + from werkzeug.routing import Rule + + app = Flask(__name__) + app.url_map.add(Rule('/', endpoint='index')) + + @app.endpoint('index') + def my_index(): + return "Hello world" + + + diff --git a/flask/app.py b/flask/app.py index 448df8fc..066215be 100644 --- a/flask/app.py +++ b/flask/app.py @@ -496,6 +496,7 @@ class Flask(_PackageBoundObject): """ options.setdefault('url_prefix', module.url_prefix) options.setdefault('subdomain', module.subdomain) + self.view_functions.update(module.view_functions) state = _ModuleSetupState(self, **options) for func in module._register_events: func(state) @@ -629,6 +630,22 @@ class Flask(_PackageBoundObject): return f return decorator + + def endpoint(self, endpoint): + """A decorator to register a function as an endpoint. + Example:: + + @app.endpoint('example.endpoint') + def example(): + return "example" + + :param endpoint: the name of the endpoint + """ + def decorator(f): + self.view_functions[endpoint] = f + return f + return decorator + def errorhandler(self, code): """A decorator that is used to register a function give a given error code. Example:: diff --git a/flask/module.py b/flask/module.py index 4e719a4e..5c914991 100644 --- a/flask/module.py +++ b/flask/module.py @@ -124,6 +124,7 @@ class Module(_PackageBoundObject): self.name = name self.url_prefix = url_prefix self.subdomain = subdomain + self.view_functions = {} self._register_events = [_register_module(self, static_path)] def route(self, rule, **options): @@ -157,6 +158,13 @@ class Module(_PackageBoundObject): view_func, **options) self._record(register_rule) + def endpoint(self, endpoint): + """Like :meth:`Flask.endpoint` but for a module.""" + def decorator(f): + self.view_functions[endpoint] = f + return f + return decorator + def before_request(self, f): """Like :meth:`Flask.before_request` but for a module. This function is only executed before each request that is handled by a function of From 785e94bf1e914c00a74b92e3549f86826d947f5b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 Jan 2011 13:50:14 +0100 Subject: [PATCH 0531/3747] Added changelog entry and entry into authors file --- AUTHORS | 1 + CHANGES | 3 +++ 2 files changed, 4 insertions(+) diff --git a/AUTHORS b/AUTHORS index 936b0b0f..0f2a9827 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,7 @@ Patches and Suggestions - Marian Sigler - Matt Campell - Matthew Frazier +- Michael van Tellingen - Ron DuPlain - Sebastien Estienne - Simon Sapin diff --git a/CHANGES b/CHANGES index 017eee1f..b718d2fd 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,9 @@ Release date to be announced, codename to be selected - Flask no longer internally depends on rules being added through the `add_url_rule` function and can now also accept regular werkzeug rules added to the url map. +- Added an `endpoint` method to the flask application object which + allows one to register a callback to an arbitrary endpoint with + a decorator. Version 0.6.1 ------------- From 4228d7281fd995beda447ee016b304b6c3764f3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 Jan 2011 13:51:06 +0100 Subject: [PATCH 0532/3747] No reference for favicon --- docs/patterns/favicon.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst index 6cf64ea0..f7b2f9c8 100644 --- a/docs/patterns/favicon.rst +++ b/docs/patterns/favicon.rst @@ -1,5 +1,3 @@ -.. _favicon: - Adding a favicon ================ From 94f90bc4eb88224d6639cbe94c4e88d8299644e2 Mon Sep 17 00:00:00 2001 From: fmw Date: Fri, 4 Feb 2011 13:01:13 +0100 Subject: [PATCH 0533/3747] updated docs on FastCGI deployment with Lighty Signed-off-by: Armin Ronacher --- docs/deploying/fastcgi.rst | 41 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 19bd42ec..eeaee43e 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -25,12 +25,13 @@ First you need to create the FastCGI server file. Let's call it #!/usr/bin/python from flup.server.fcgi import WSGIServer from yourapplication import app + + if __name__ == '__main__': + WSGIServer(app).run() - WSGIServer(app).run() - -This is enough for Apache to work, however lighttpd and nginx need a -socket to communicate with the FastCGI server. For that to work you -need to pass the path to the socket to the +This is enough for Apache to work, however nginx and older versions of +lighttpd need a socket to be explicitly passed to communicate with the FastCGI +server. For that to work you need to pass the path to the socket to the :class:`~flup.server.fcgi.WSGIServer`:: WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() @@ -54,21 +55,33 @@ Configuring lighttpd A basic FastCGI configuration for lighttpd looks like that:: - fastcgi.server = ("/yourapplication" => - "yourapplication" => ( + fastcgi.server = ("/yourapplication.fcgi" => + (( "socket" => "/tmp/yourapplication-fcgi.sock", "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", - "check-local" => "disable" - ) + "check-local" => "disable", + "max-procs" -> 1 + )) ) -This configuration binds the application to `/yourapplication`. If you -want the application to work in the URL root you have to work around a -lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` -middleware. + alias.url = ( + "/static/" => "/path/to/your/static" + ) + + url.rewrite-once = ( + "^(/static.*)$" => "$1", + "^(/.*)$" => "/yourapplication.fcgi$1" + +Remember to enable the FastCGI, alias and rewrite modules. This configuration +binds the application to `/yourapplication`. If you want the application to +work in the URL root you have to work around a lighttpd bug with the +:class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. Make sure to apply it only if you are mounting the application the URL -root. +root. Also, see the Lighty docs for more information on `FastCGI and Python +`_ (note that +explicitly passing a socket to run() is no longer necessary). + Configuring nginx ----------------- From 0bf777ed81d5296fb722931d35e5db4333e3de39 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 5 Feb 2011 17:41:02 +0100 Subject: [PATCH 0534/3747] Do not use Date, use Last-Modified. This fixes #164 --- CHANGES | 2 ++ flask/helpers.py | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index b718d2fd..2ca6e355 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,8 @@ Release date to be announced, codename to be selected - Added an `endpoint` method to the flask application object which allows one to register a callback to an arbitrary endpoint with a decorator. +- Use Last-Modified for static file sending instead of Date which + was incorrectly introduced in 0.6. Version 0.6.1 ------------- diff --git a/flask/helpers.py b/flask/helpers.py index eb12eee6..ed8a5d5c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -364,13 +364,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, direct_passthrough=True) # if we know the file modification date, we can store it as the - # current time to better support conditional requests. Werkzeug - # as of 0.6.1 will override this value however in the conditional - # response with the current time. This will be fixed in Werkzeug - # with a new release, however many WSGI servers will still emit - # a separate date header. + # the time of the last modification. if mtime is not None: - rv.date = int(mtime) + rv.last_modified = int(mtime) rv.cache_control.public = True if cache_timeout: From 6562f2e152222e473d07e24f43f1cfe091675854 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 6 Feb 2011 11:56:55 +0100 Subject: [PATCH 0535/3747] Fixed line numbers for flask pdf --- docs/flaskstyle.sty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/flaskstyle.sty b/docs/flaskstyle.sty index b2d073d0..8a3f75c3 100644 --- a/docs/flaskstyle.sty +++ b/docs/flaskstyle.sty @@ -71,7 +71,7 @@ \renewcommand\thepart{\@Roman\c@part} \renewcommand\part{% - \pagestyle{empty} + \pagestyle{plain} \if@noskipsec \leavevmode \fi \cleardoublepage \vspace*{6cm}% From 8ed051d5b5f136397e26dc20845869366afc2cae Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 10 Feb 2011 21:43:13 +0100 Subject: [PATCH 0536/3747] Fixed a terrible wrong part in the module documentation --- docs/patterns/packages.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 4ec09e6b..a6c7cce0 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -248,22 +248,25 @@ If you want to refer to the templates you just have to prefix it with the name of the module. So for the admin it would be ``render_template('admin/list_items.html')`` and so on. It is not possible to refer to templates without the prefixed module name. This is -explicit unlike URL rules. +explicit unlike URL rules. Also with the move of the views into from +`yourapplication.views.admin` too `yourapplication.apps.admin.views` you +will have to give the module an explit shortname. Why? Because otherwise +all your modules will be internally known as `views` which is obviously +not what you want:: -You also need to explicitly pass the ``url_prefix`` argument when -registering your modules this way:: + # in yourapplication/apps/admin/views.py + admin = Module(__name__, 'admin') + +The setup code changes slightly because of the imports:: # in yourapplication/__init__.py from flask import Flask from yourapplication.apps.admin.views import admin from yourapplication.apps.frontend.views import frontend - app = Flask(__name__) app.register_module(admin, url_prefix='/admin') - app.register_module(frontend, url_prefix='/frontend') - -This is because Flask cannot infer the prefix from the package names. + app.register_module(frontend) .. admonition:: References to Static Folders From 09a06abb774062e72d106fa2a732dcad8fd96c56 Mon Sep 17 00:00:00 2001 From: Jerry Baker Date: Wed, 3 Nov 2010 07:38:48 -0400 Subject: [PATCH 0537/3747] Updated 'declarative' link in docs/patterns/sqlalchemy.rst Signed-off-by: Armin Ronacher --- docs/patterns/sqlalchemy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 97317881..24e9f013 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -110,7 +110,7 @@ Querying is simple as well: .. _SQLAlchemy: http://www.sqlalchemy.org/ .. _declarative: - http://www.sqlalchemy.org/docs/reference/ext/declarative.html + http://www.sqlalchemy.org/docs/orm/extensions/declarative.html Manual Object Relational Mapping -------------------------------- From 1643e5c39157e4f43d20b965c9967eeec8e90700 Mon Sep 17 00:00:00 2001 From: Sean Reifschneider Date: Sun, 6 Feb 2011 14:14:51 -0700 Subject: [PATCH 0538/3747] Fixing a word usage issue in docs. Signed-off-by: Armin Ronacher --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 066215be..39445ab3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -715,7 +715,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not catched. In debug mode the exception will + occours that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. From b49de851d943c24a565488a51b8f11fbfd5ad796 Mon Sep 17 00:00:00 2001 From: Baiju M Date: Sun, 20 Feb 2011 10:50:38 +0530 Subject: [PATCH 0539/3747] Link to issue tracker Signed-off-by: Armin Ronacher --- docs/_templates/sidebarintro.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index f2aecc74..164c8745 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -18,4 +18,5 @@

  • The Flask Website
  • Flask @ PyPI
  • Flask @ github
  • +
  • Issue Tracker
  • From 00c5b7a937b63378394fc6fc576aa6c27106d6d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Feb 2011 21:56:37 +0100 Subject: [PATCH 0540/3747] added create_jinja_loader --- CHANGES | 1 + flask/app.py | 10 +++++++++- tests/flask_tests.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 2ca6e355..9127c126 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,7 @@ Release date to be announced, codename to be selected a decorator. - Use Last-Modified for static file sending instead of Date which was incorrectly introduced in 0.6. +- Added `create_jinja_loader` to override the loader creation process. Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index 39445ab3..c247422a 100644 --- a/flask/app.py +++ b/flask/app.py @@ -352,7 +352,15 @@ class Flask(_PackageBoundObject): options = dict(self.jinja_options) if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape - return Environment(loader=_DispatchingJinjaLoader(self), **options) + return Environment(loader=self.create_jinja_loader(), **options) + + def create_jinja_loader(self): + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. + + .. versionadded:: 0.7 + """ + return _DispatchingJinjaLoader(self) def init_jinja_globals(self): """Called directly after the environment was created to inject diff --git a/tests/flask_tests.py b/tests/flask_tests.py index db2616a9..04b54777 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -857,6 +857,19 @@ class TemplatingTestCase(unittest.TestCase): rv = app.test_client().get('/') assert rv.data == 'dcba' + def test_custom_template_loader(self): + class MyFlask(flask.Flask): + def create_jinja_loader(self): + from jinja2 import DictLoader + return DictLoader({'index.html': 'Hello Custom World!'}) + app = MyFlask(__name__) + @app.route('/') + def index(): + return flask.render_template('index.html') + c = app.test_client() + rv = c.get('/') + assert rv.data == 'Hello Custom World!' + class ModuleTestCase(unittest.TestCase): From 4141afa22b6a52279098ef39413f97773c1b75bf Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 15:32:19 -0900 Subject: [PATCH 0541/3747] fixing cross-reference links on API doc page --- docs/api.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 50c9820d..fb604abf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -40,24 +40,24 @@ Incoming Request Data This is a proxy. See :ref:`notes-on-proxies` for more information. - The request object is an instance of a :class:`~werkzeug.Request` + The request object is an instance of a :class:`~werkzeug.wrappers.Request` subclass and provides all of the attributes Werkzeug defines. This just shows a quick overview of the most important ones. .. attribute:: form - A :class:`~werkzeug.MultiDict` with the parsed form data from `POST` + A :class:`~werkzeug.datastructures.MultiDict` with the parsed form data from `POST` or `PUT` requests. Please keep in mind that file uploads will not end up here, but instead in the :attr:`files` attribute. .. attribute:: args - A :class:`~werkzeug.MultiDict` with the parsed contents of the query + A :class:`~werkzeug.datastructures.MultiDict` with the parsed contents of the query string. (The part in the URL after the question mark). .. attribute:: values - A :class:`~werkzeug.CombinedMultiDict` with the contents of both + A :class:`~werkzeug.datastructures.CombinedMultiDict` with the contents of both :attr:`form` and :attr:`args`. .. attribute:: cookies @@ -79,11 +79,11 @@ Incoming Request Data .. attribute:: files - A :class:`~werkzeug.MultiDict` with files uploaded as part of a + A :class:`~werkzeug.datastructures.MultiDict` with files uploaded as part of a `POST` or `PUT` request. Each file is stored as - :class:`~werkzeug.FileStorage` object. It basically behaves like a + :class:`~werkzeug.datastructures.FileStorage` object. It basically behaves like a standard file object you know from Python, with the difference that - it also has a :meth:`~werkzeug.FileStorage.save` function that can + it also has a :meth:`~werkzeug.datastructures.FileStorage.save` function that can store the file on the filesystem. .. attribute:: environ @@ -228,7 +228,7 @@ Useful Functions and Classes .. function:: abort(code) - Raises an :exc:`~werkzeug.exception.HTTPException` for the given + Raises an :exc:`~werkzeug.exceptions.HTTPException` for the given status code. For example to abort request handling with a page not found exception, you would call ``abort(404)``. @@ -308,7 +308,7 @@ Useful Internals .. data:: _request_ctx_stack - The internal :class:`~werkzeug.LocalStack` that is used to implement + The internal :class:`~werkzeug.local.LocalStack` that is used to implement all the context local objects used in Flask. This is a documented instance and can be used by extensions and application code but the use is discouraged in general. @@ -435,7 +435,7 @@ exceptions where it is good to know that this object is an actual proxy: :ref:`signals`) If you need to get access to the underlying object that is proxied, you -can use the :meth:`~werkzeug.LocalProxy._get_current_object` method:: +can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: app = current_app._get_current_object() my_signal.send(app) From 6bdde49348f6c4634a42b76c3c66fad163a2e3e6 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 15:47:14 -0900 Subject: [PATCH 0542/3747] fixing cross-referenced links on testing page --- docs/testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index b9f0806b..6c7b8428 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -13,7 +13,7 @@ safely change things, and you will instantly know if your change broke something. Flask gives you a couple of ways to test applications. It mainly does -that by exposing the Werkzeug test :class:`~werkzeug.Client` class to your +that by exposing the Werkzeug test :class:`~werkzeug.test.Client` class to your code and handling the context locals for you. You can then use that with your favourite testing solution. In this documentation we will use the :mod:`unittest` package that comes preinstalled with each Python @@ -110,7 +110,7 @@ Test functions begin with the word `test`. Every function named like that will be picked up automatically. By using `self.app.get` we can send an HTTP `GET` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now -use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the +use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure that ``'No entries here so far'`` is part of the output. From 4046d3bd30729324ff4548a42dfcc2d4f821bb5d Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 15:47:40 -0900 Subject: [PATCH 0543/3747] fixing cross-referenced links on fileupload patterns page --- docs/patterns/fileuploads.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 221ce327..23d6c6bb 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -10,7 +10,7 @@ uploads is actually quite simple. It basically works like this: and an ```` is placed in that form. 2. The application accesses the file from the :attr:`~flask.request.files` dictionary on the request object. -3. use the :meth:`~werkzeug.FileStorage.save` method of the file to save +3. use the :meth:`~werkzeug.datastructures.FileStorage.save` method of the file to save the file permanently somewhere on the filesystem. A Gentle Introduction @@ -71,7 +71,7 @@ the file and redirects the user to the URL for the uploaded file:: ''' -So what does that :func:`~werkzeug.secure_filename` function actually do? +So what does that :func:`~werkzeug.utils.secure_filename` function actually do? Now the problem is that there is that principle called "never trust user input". This is also true for the filename of an uploaded file. All submitted form data can be forged, and filenames can be dangerous. For @@ -80,7 +80,7 @@ before storing it directly on the filesystem. .. admonition:: Information for the Pros - So you're interested in what that :func:`~werkzeug.secure_filename` + So you're interested in what that :func:`~werkzeug.utils.secure_filename` function does and what the problem is if you're not using it? So just imagine someone would send the following information as `filename` to your application:: @@ -109,7 +109,7 @@ Flask 0.5 we can use a function that does that for us:: filename) Alternatively you can register `uploaded_file` as `build_only` rule and -use the :class:`~werkzeug.SharedDataMiddleware`. This also works with +use the :class:`~werkzeug.wsgi.SharedDataMiddleware`. This also works with older versions of Flask:: from werkzeug import SharedDataMiddleware From 15ae22494df135b0e332aefb9abc43fb42a87d4c Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 15:47:57 -0900 Subject: [PATCH 0544/3747] fixing cross-referenced links on quickstart page --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c115fa70..b4f6027f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -548,7 +548,7 @@ filesystem. You can access those files by looking at the :attr:`~flask.request.files` attribute on the request object. Each uploaded file is stored in that dictionary. It behaves just like a standard Python :class:`file` object, but it also has a -:meth:`~werkzeug.FileStorage.save` method that allows you to store that +:meth:`~werkzeug.datastructures.FileStorage.save` method that allows you to store that file on the filesystem of the server. Here is a simple example showing how that works:: @@ -563,10 +563,10 @@ that works:: If you want to know how the file was named on the client before it was uploaded to your application, you can access the -:attr:`~werkzeug.FileStorage.filename` attribute. However please keep in +:attr:`~werkzeug.datastructures.FileStorage.filename` attribute. However please keep in mind that this value can be forged so never ever trust that value. If you want to use the filename of the client to store the file on the server, -pass it through the :func:`~werkzeug.secure_filename` function that +pass it through the :func:`~werkzeug.utils.secure_filename` function that Werkzeug provides for you:: from flask import request From 5764f3abd2e8e211545f9cb362e95e30a76a87d3 Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 15:48:19 -0900 Subject: [PATCH 0545/3747] fixing cross-referenced links on jquery patterns page --- docs/patterns/jquery.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 46285864..d4fe6d4e 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -108,7 +108,7 @@ template. This template will load jQuery as above and have a little form we can add two numbers and a link to trigger the function on the server side. -Note that we are using the :meth:`~werkzeug.MultiDict.get` method here +Note that we are using the :meth:`~werkzeug.datastructures.MultiDict.get` method here which will never fail. If the key is missing a default value (here ``0``) is returned. Furthermore it can convert values to a specific type (like in our case `int`). This is especially handy for code that is From da00160db61050da73213ecede1fffaf42f2fdf6 Mon Sep 17 00:00:00 2001 From: Sam Anderson Date: Sun, 13 Mar 2011 19:26:40 -0400 Subject: [PATCH 0546/3747] Fixed some small typos in the documentation. Signed-off-by: Armin Ronacher --- docs/config.rst | 2 +- docs/errorhandling.rst | 2 +- docs/extensiondev.rst | 6 +++--- docs/security.rst | 4 ++-- docs/styleguide.rst | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index de74aa2b..90a276cc 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -208,7 +208,7 @@ configuration:: class DevelopmentConfig(Config): DEBUG = True - class TestinConfig(Config): + class TestingConfig(Config): TESTING = True To enable such a config you just have to call into diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index c216c160..debb9d75 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -105,7 +105,7 @@ above, just make sure to use a lower setting (I would recommend if not app.debug: import logging - from themodule import TheHandler YouWant + from themodule import TheHandlerYouWant file_handler = TheHandlerYouWant(...) file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 1848ca8f..e468d8a1 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -288,12 +288,12 @@ extension to be approved you have to follow these guidelines: 1. An approved Flask extension must provide exactly one package or module inside the `flaskext` namespace package. -2. It must ship a testsuite that can either be invoked with ``make test`` - or ``python setup.py test``. For testsuites invoked with ``make +2. It must ship a testing suite that can either be invoked with ``make test`` + or ``python setup.py test``. For test suites invoked with ``make test`` the extension has to ensure that all dependencies for the test are installed automatically, in case of ``python setup.py test`` dependencies for tests alone can be specified in the `setup.py` - file. The testsuite also has to be part of the distribution. + file. The test suite also has to be part of the distribution. 3. APIs of approved extensions will be checked for the following characteristics: diff --git a/docs/security.rst b/docs/security.rst index a8a0afcf..35afd49e 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -110,7 +110,7 @@ stuff. Unfortunately that protection is only there for generate JSON. So what is the issue and how to avoid it? The problem are arrays at -toplevel in JSON. Imagine you send the following data out in a JSON +top-level in JSON. Imagine you send the following data out in a JSON request. Say that's exporting the names and email addresses of all your friends for a part of the user interface that is written in JavaScript. Not very uncommon: @@ -156,7 +156,7 @@ possible to patch constructors and register callbacks for setters. An attacker can use this (like above) to get all the data you exported in your JSON file. The browser will totally ignore the ``application/json`` mimetype if ``text/javascript`` is defined as content type in the script -tag and evaluate that as JavaScript. Because toplevel array elements are +tag and evaluate that as JavaScript. Because top-level array elements are allowed (albeit useless) and we hooked in our own constructor, after that page loaded the data from the JSON response is in the `captured` array. diff --git a/docs/styleguide.rst b/docs/styleguide.rst index 0fdc88d8..d46ecd04 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -91,7 +91,7 @@ General whitespace rules: value = my_list[ index ] value = my_dict ['key'] -Yoda statements are a nogo: +Yoda statements are a no-go: Never compare constant with variable, always variable with constant: Good:: From b3fc9eb36b8021ad966be57532a5fa5ae4231e13 Mon Sep 17 00:00:00 2001 From: mvantellingen Date: Fri, 11 Mar 2011 15:33:35 +0100 Subject: [PATCH 0547/3747] Open the session after the request context is pushed on the stack instead of in the constructor. This allows you to access the request context in a custom open_session method. Signed-off-by: Armin Ronacher --- flask/ctx.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flask/ctx.py b/flask/ctx.py index 1b17086c..d3ac231d 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -30,11 +30,9 @@ class _RequestContext(object): self.app = app self.request = app.request_class(environ) self.url_adapter = app.create_url_adapter(self.request) - self.session = app.open_session(self.request) - if self.session is None: - self.session = _NullSession() self.g = _RequestGlobals() self.flashes = None + self.session = None try: url_rule, self.request.view_args = \ @@ -47,6 +45,13 @@ class _RequestContext(object): """Binds the request context.""" _request_ctx_stack.push(self) + # Open the session at the moment that the request context is + # available. This allows a custom open_session method to use the + # request context (e.g. flask-sqlalchemy). + self.session = self.app.open_session(self.request) + if self.session is None: + self.session = _NullSession() + def pop(self): """Pops the request context.""" _request_ctx_stack.pop() From 5cc40f4728d5b5356cf49b7dfaa2b130e0b0aedf Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Mon, 14 Mar 2011 10:36:33 -0400 Subject: [PATCH 0548/3747] silent option added to 'from_pyfile' to mirror 'from_envvar'. --- flask/config.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flask/config.py b/flask/config.py index aa65f460..2b7499a4 100644 --- a/flask/config.py +++ b/flask/config.py @@ -83,13 +83,13 @@ class Config(dict): def from_envvar(self, variable_name, silent=False): """Loads a configuration from an environment variable pointing to - a configuration file. This basically is just a shortcut with nicer + a configuration file. This is basically just a shortcut with nicer error messages for this line of code:: app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent failing for missing + :param silent: set to `True` if you want silent to fail for missing files. :return: bool. `True` if able to load config, `False` otherwise. """ @@ -105,7 +105,7 @@ class Config(dict): self.from_pyfile(rv) return True - def from_pyfile(self, filename): + def from_pyfile(self, filename, silent=False): """Updates the values in the config from a Python file. This function behaves as if the file was imported as module with the :meth:`from_object` function. @@ -113,6 +113,8 @@ class Config(dict): :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the root path. + :param silent: set to `True` if you want silent to fail for missing + files. """ filename = os.path.join(self.root_path, filename) d = imp.new_module('config') @@ -120,9 +122,12 @@ class Config(dict): try: execfile(filename, d.__dict__) except IOError, e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False e.strerror = 'Unable to load configuration file (%s)' % e.strerror raise self.from_object(d) + return True def from_object(self, obj): """Updates the values from the given object. An object can be of one From fa9817778c83eb35b9c2c4332ccb7e5190d1ffa2 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Mon, 14 Mar 2011 10:41:22 -0400 Subject: [PATCH 0549/3747] Test passes. Added test for silent flag; added import of errno so it passed. --- flask/config.py | 1 + tests/flask_tests.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index 2b7499a4..250a8774 100644 --- a/flask/config.py +++ b/flask/config.py @@ -14,6 +14,7 @@ from __future__ import with_statement import imp import os import sys +import errno from werkzeug import import_string diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 04b54777..42d4d3d0 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1310,7 +1310,7 @@ class ConfigTestCase(unittest.TestCase): assert "'FOO_SETTINGS' is not set" in str(e) else: assert 0, 'expected exception' - not app.config.from_envvar('FOO_SETTINGS', silent=True) + assert not app.config.from_envvar('FOO_SETTINGS', silent=True) os.environ = {'FOO_SETTINGS': 'flask_tests.py'} assert app.config.from_envvar('FOO_SETTINGS') @@ -1329,6 +1329,7 @@ class ConfigTestCase(unittest.TestCase): assert msg.endswith("missing.cfg'") else: assert 0, 'expected config' + assert not app.config.from_pyfile('missing.cfg', silent=True) class SubdomainTestCase(unittest.TestCase): From 45b97d14e3ab7a8b7e6010bb6490a87b00c2ae2b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Mar 2011 14:19:12 -0400 Subject: [PATCH 0550/3747] Started work on app dispatch docs --- docs/patterns/appdispatch.rst | 78 +++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 2 files changed, 79 insertions(+) create mode 100644 docs/patterns/appdispatch.rst diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst new file mode 100644 index 00000000..91858450 --- /dev/null +++ b/docs/patterns/appdispatch.rst @@ -0,0 +1,78 @@ +.. _app-dispatch: + +Application Dispatching +======================= + +Sometimes you might want to use multiple instances of the same application +with different configurations. Assuming the application is created inside +a function and you can call that function to instanciate it, that is +really easy to implement. In order to develop your application to support +creating new instances in functions have a look at the +:ref:`app-factories` pattern. + + +Dispatch by Subdomain +--------------------- + +A very common example would be creating applications per subdomain. For +instance you configure your webserver to dispatch all requests for all +subdomains to your application and you then use the subdomain information +to create user-specific instances. + +Once you have your server set up to listen on all subdomains you can use a +very simple WSGI application to do the dynamic application creation. + +The code for the dispatching looks roughly like this: + +.. sourcecode:: python + + from threading import Lock + + class SubdomainDispatcher(object): + + def __init__(self, domain, create_app): + self.domain = domain + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, host): + host = host.split(':')[0] + assert host.endswith(self.domain), 'Configuration error' + subdomain = host[:-len(self.domain)].rstrip('.') + with self.lock: + app = self.instances.get(subdomain) + if app is None: + app = self.make_app(subdomain) + self.instances[subdomain] = app + return app + + def make_app(self, subdomain): + return self.create_app(subdomain) + + def __call__(self, environ, start_response): + app = self.get_application(environ['HTTP_HOST']) + return app(environ, start_response) + + +If you want to use it, you can do something like this: + +.. sourcecode:: python + + from myapplication import create_app, get_user_for_subdomain + from werkzeug.exceptions import NotFound + + def make_app(subdomain): + user = get_user_for_subdomain(subdomain) + if user is None: + # if there is no user for that subdomain we still have + # to return a WSGI application that handles that request. + # We can then just return the NotFound() exception as + # application which will render a default 404 page. + # You might also redirect the user to the main page then + return NotFound() + + # otherwise create the application for the specific user + return create_app(user) + + application = SubdomainDispatcher('example.com', make_app) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 68cff143..ed231163 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -18,6 +18,7 @@ Snippet Archives `_. packages appfactories + appdispatch distribute fabric sqlite3 From 34a494713c085926d023f8e8f9cf6f0c40bab660 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Mar 2011 14:21:28 -0400 Subject: [PATCH 0551/3747] Documented some changes in config handling. --- CHANGES | 1 + flask/config.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 9127c126..18c1494e 100644 --- a/CHANGES +++ b/CHANGES @@ -36,6 +36,7 @@ Release date to be announced, codename to be selected - Use Last-Modified for static file sending instead of Date which was incorrectly introduced in 0.6. - Added `create_jinja_loader` to override the loader creation process. +- Implemented a silent flag for `config.from_pyfile`. Version 0.6.1 ------------- diff --git a/flask/config.py b/flask/config.py index 250a8774..920d4835 100644 --- a/flask/config.py +++ b/flask/config.py @@ -116,6 +116,9 @@ class Config(dict): root path. :param silent: set to `True` if you want silent to fail for missing files. + + .. versionadded:: 0.7 + `silent` parameter. """ filename = os.path.join(self.root_path, filename) d = imp.new_module('config') From 235369fd613b72c1767b2b20d9f94dee1d6090d5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Mar 2011 14:21:44 -0400 Subject: [PATCH 0552/3747] Removed unnecessary import --- flask/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index 920d4835..b276f168 100644 --- a/flask/config.py +++ b/flask/config.py @@ -13,7 +13,6 @@ from __future__ import with_statement import imp import os -import sys import errno from werkzeug import import_string From 3deae1bd48bf7dfad863afae4d368993fc9bafe1 Mon Sep 17 00:00:00 2001 From: Drew Vogel Date: Wed, 9 Mar 2011 12:31:39 -0600 Subject: [PATCH 0553/3747] Clarified language related to avoiding circular imports. Signed-off-by: Armin Ronacher --- docs/patterns/packages.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index a6c7cce0..e1b92c08 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -56,8 +56,8 @@ following quick checklist: `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` decorator on top) have to be imported when in the `__init__.py` file. - Not the object itself, but the module it is in. Do the importing at - the *bottom* of the file. + Not the object itself, but the module it is in. Import the view module + *after the application object is created*. Here's an example `__init__.py`:: @@ -158,10 +158,10 @@ Do the same with the `frontend.py` and then make sure to register the modules in the application (`__init__.py`) like this:: from flask import Flask + app = Flask(__name__) + from yourapplication.views.admin import admin from yourapplication.views.frontend import frontend - - app = Flask(__name__) app.register_module(admin, url_prefix='/admin') app.register_module(frontend) From 04e70bd5c7bd80696eabaea11afd9080d874f859 Mon Sep 17 00:00:00 2001 From: Matt Chisholm Date: Mon, 14 Mar 2011 13:00:34 -0400 Subject: [PATCH 0554/3747] Add teardown_request decorator. Fixes issue #174 --- CHANGES | 2 ++ docs/tutorial/dbcon.rst | 25 ++++++++++++-- flask/app.py | 39 ++++++++++++++++++++++ tests/flask_tests.py | 74 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 18c1494e..32329ef8 100644 --- a/CHANGES +++ b/CHANGES @@ -37,6 +37,8 @@ Release date to be announced, codename to be selected was incorrectly introduced in 0.6. - Added `create_jinja_loader` to override the loader creation process. - Implemented a silent flag for `config.from_pyfile`. +- Added `teardown_request` decorator, for functions that should run at the end + of a request regardless of whether an exception occurred. Version 0.6.1 ------------- diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 50aba04d..f700a329 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -8,8 +8,11 @@ but how can we elegantly do that for requests? We will need the database connection in all our functions so it makes sense to initialize them before each request and shut them down afterwards. -Flask allows us to do that with the :meth:`~flask.Flask.before_request` and -:meth:`~flask.Flask.after_request` decorators:: +Flask allows us to do that with the :meth:`~flask.Flask.before_request`, +:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request` +decorators. In debug mode, if an error is raised, +:meth:`~flask.Flask.after_request` won't be run, and you'll have access to the +db connection in the interactive debugger:: @app.before_request def before_request(): @@ -20,13 +23,29 @@ Flask allows us to do that with the :meth:`~flask.Flask.before_request` and g.db.close() return response +If you want to guarantee that the connection is always closed in debug mode, you +can close it in a function decorated with :meth:`~flask.Flask.teardown_request`: + + @app.before_request + def before_request(): + g.db = connect_db() + + @app.teardown_request + def teardown_request(exception): + g.db.close() + Functions marked with :meth:`~flask.Flask.before_request` are called before -a request and passed no arguments, functions marked with +a request and passed no arguments. Functions marked with :meth:`~flask.Flask.after_request` are called after a request and passed the response that will be sent to the client. They have to return that response object or a different one. In this case we just return it unchanged. +Functions marked with :meth:`~flask.Flask.teardown_request` get called after the +response has been constructed. They are not allowed to modify the request, and +their return values are ignored. If an exception occurred while the request was +being processed, it is passed to each function; otherwise, None is passed in. + We store our current database connection on the special :data:`~flask.g` object that flask provides for us. This object stores information for one request only and is available from within each function. Never store such diff --git a/flask/app.py b/flask/app.py index c247422a..fad49945 100644 --- a/flask/app.py +++ b/flask/app.py @@ -11,6 +11,7 @@ from __future__ import with_statement +import sys from threading import Lock from datetime import timedelta, datetime from itertools import chain @@ -247,6 +248,18 @@ class Flask(_PackageBoundObject): #: :meth:`after_request` decorator. self.after_request_funcs = {} + #: A dictionary with lists of functions that are called after + #: each request, even if an exception has occurred. The key of the + #: dictionary is the name of the module this function is active for, + #: `None` for all requests. These functions are not allowed to modify + #: the request, and their return values are ignored. If an exception + #: occurred while processing the request, it gets passed to each + #: teardown_request function. To register a function here, use the + #: :meth:`teardown_request` decorator. + #: + #: .. versionadded:: 0.7 + self.teardown_request_funcs = {} + #: A dictionary with list of functions that are called without argument #: to populate the template context. The key of the dictionary is the #: name of the module this function is active for, `None` for all @@ -704,6 +717,11 @@ class Flask(_PackageBoundObject): self.after_request_funcs.setdefault(None, []).append(f) return f + def teardown_request(self, f): + """Register a function to be run at the end of each request, regardless of whether there was an exception or not.""" + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + def context_processor(self, f): """Registers a template context processor function.""" self.template_context_processors[None].append(f) @@ -869,6 +887,20 @@ class Flask(_PackageBoundObject): response = handler(response) return response + def do_teardown_request(self): + """Called after the actual request dispatching and will + call every as :meth:`teardown_request` decorated function. + """ + funcs = reversed(self.teardown_request_funcs.get(None, ())) + mod = request.module + if mod and mod in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[mod])) + exc = sys.exc_info()[1] + for func in funcs: + rv = func(exc) + if rv is not None: + return rv + def request_context(self, environ): """Creates a request context from the given environment and binds it to the current context. This must be used in combination with @@ -947,6 +979,11 @@ class Flask(_PackageBoundObject): even if an exception happens database have the chance to properly close the connection. + .. versionchanged:: 0.7 + The :meth:`teardown_request` functions get called at the very end of + processing the request. If an exception was thrown, it gets passed to + each teardown_request function. + :param environ: a WSGI environment :param start_response: a callable accepting a status code, a list of headers and an optional @@ -965,6 +1002,8 @@ class Flask(_PackageBoundObject): response = self.process_response(response) except Exception, e: response = self.make_response(self.handle_exception(e)) + finally: + self.do_teardown_request() request_finished.send(self, response=response) return response(environ, start_response) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 42d4d3d0..e3631202 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -413,6 +413,72 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Internal Server Error' in rv.data assert len(called) == 1 + def test_teardown_request_handler(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert 'Response' in rv.data + assert len(called) == 1 + + def test_teardown_request_handler_debug_mode(self): + called = [] + app = flask.Flask(__name__) + app.debug = True + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert 'Response' in rv.data + assert len(called) == 1 + + + def test_teardown_request_handler_error(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request1(exc): + assert type(exc) == ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.teardown_request + def teardown_request2(exc): + assert type(exc) == ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + assert len(called) == 2 + + def test_before_after_request_order(self): called = [] app = flask.Flask(__name__) @@ -430,12 +496,18 @@ class BasicFunctionalityTestCase(unittest.TestCase): def after2(response): called.append(3) return response + @app.teardown_request + def finish1(exc): + called.append(6) + @app.teardown_request + def finish2(exc): + called.append(5) @app.route('/') def index(): return '42' rv = app.test_client().get('/') assert rv.data == '42' - assert called == [1, 2, 3, 4] + assert called == [1, 2, 3, 4, 5, 6] def test_error_handling(self): app = flask.Flask(__name__) From f58c98904f83aeff934b66d87b4e42a06ceb840d Mon Sep 17 00:00:00 2001 From: matt swanson Date: Sun, 13 Mar 2011 17:18:19 -0900 Subject: [PATCH 0555/3747] fixing some wording issues on the testing page Signed-off-by: Armin Ronacher --- docs/testing.rst | 111 +++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 6c7b8428..48af2a18 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -5,26 +5,23 @@ Testing Flask Applications **Something that is untested is broken.** -Not sure where that is coming from, and it's not entirely correct, but -also not that far from the truth. Untested applications make it hard to +The origin of this quote is unknown and while it is not entirely correct, it is also +not far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to become pretty paranoid. If an application has automated tests, you can -safely change things, and you will instantly know if your change broke -something. +safely make changes and instantly know if anything breaks. -Flask gives you a couple of ways to test applications. It mainly does -that by exposing the Werkzeug test :class:`~werkzeug.test.Client` class to your -code and handling the context locals for you. You can then use that with -your favourite testing solution. In this documentation we will use the -:mod:`unittest` package that comes preinstalled with each Python -installation. +Flask provides a way to test your application by exposing the Werkzeug +test :class:`~werkzeug.test.Client` and handling the context locals for you. +You can then use that with your favourite testing solution. In this documentation +we will use the :mod:`unittest` package that comes pre-installed with Python. The Application --------------- -First we need an application to test for functionality. For the testing -we will use the application from the :ref:`tutorial`. If you don't have -that application yet, get the sources from `the examples`_. +First, we need an application to test; we will use the application from +the :ref:`tutorial`. If you don't have that application yet, get the +sources from `the examples`_. .. _the examples: http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ @@ -32,8 +29,8 @@ that application yet, get the sources from `the examples`_. The Testing Skeleton -------------------- -In order to test that, we add a second module ( -`flaskr_tests.py`) and create a unittest skeleton there:: +In order to test the application, we add a second module +(`flaskr_tests.py`) and create a unittest skeleton there:: import os import flaskr @@ -55,13 +52,14 @@ In order to test that, we add a second module ( unittest.main() The code in the :meth:`~unittest.TestCase.setUp` method creates a new test -client and initializes a new database. That function is called before -each individual test function. To delete the database after the test, we -close the file and remove it from the filesystem in the -:meth:`~unittest.TestCase.tearDown` method. What the test client does is -give us a simple interface to the application. We can trigger test -requests to the application, and the client will also keep track of cookies -for us. +client and initializes a new database. This function is called before +each individual test function is run. To delete the database after the +test, we close the file and remove it from the filesystem in the +:meth:`~unittest.TestCase.tearDown` method. + +This test client will give us a simple interface to the application. We can +trigger test requests to the application, and the client will also keep track +of cookies for us. Because SQLite3 is filesystem-based we can easily use the tempfile module to create a temporary database and initialize it. The @@ -70,7 +68,7 @@ low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. -If we now run that test suite, we should see the following output:: +If we now run the test suite, we should see the following output:: $ python flaskr_tests.py @@ -79,17 +77,17 @@ If we now run that test suite, we should see the following output:: OK -Even though it did not run any tests, we already know that our flaskr +Even though it did not run any actual tests, we already know that our flaskr application is syntactically valid, otherwise the import would have died with an exception. The First Test -------------- -Now we can add the first test. Let's check that the application shows -"No entries here so far" if we access the root of the application (``/``). -For that we modify our created test case class so that it looks like -this:: +Now it's time to start testing the functionality of the application. +Let's check that the application shows "No entries here so far" if we +access the root of the application (``/``). To do this, we add a new +test method to our class, like this:: class FlaskrTestCase(unittest.TestCase): @@ -106,13 +104,14 @@ this:: rv = self.app.get('/') assert 'No entries here so far' in rv.data -Test functions begin with the word `test`. Every function named like that -will be picked up automatically. By using `self.app.get` we can send an -HTTP `GET` request to the application with the given path. The return -value will be a :class:`~flask.Flask.response_class` object. We can now -use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect the -return value (as string) from the application. In this case, we ensure -that ``'No entries here so far'`` is part of the output. +Notice that our test functions begin with the word `test`; this allows +:mod:`unittest` to automatically identify the method as a test to run. + +By using `self.app.get` we can send an HTTP `GET` request to the application with +the given path. The return value will be a :class:`~flask.Flask.response_class` object. +We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect +the return value (as string) from the application. In this case, we ensure that +``'No entries here so far'`` is part of the output. Run it again and you should see one passing test:: @@ -123,18 +122,14 @@ Run it again and you should see one passing test:: OK -Of course you can submit forms with the test client as well, which we will -use now to log our user in. - Logging In and Out ------------------ The majority of the functionality of our application is only available for -the administrative user, so we need a way to log our test client in to the -application and out of it again. For that we fire some requests to the -login and logout pages with the required form data (username and -password). Because the login and logout pages redirect, we tell the -client to `follow_redirects`. +the administrative user, so we need a way to log our test client in and out +of the application. To do this, we fire some requests to the login and logout +pages with the required form data (username and password). And because the +login and logout pages redirect, we tell the client to `follow_redirects`. Add the following two methods to your `FlaskrTestCase` class:: @@ -147,7 +142,7 @@ Add the following two methods to your `FlaskrTestCase` class:: def logout(self): return self.app.get('/logout', follow_redirects=True) -Now we can easily test if logging in and out works and that it fails with +Now we can easily test that logging in and out works and that it fails with invalid credentials. Add this new test to the class:: def test_login_logout(self): @@ -163,7 +158,7 @@ invalid credentials. Add this new test to the class:: Test Adding Messages -------------------- -Now we can also test that adding messages works. Add a new test method +We should also test that adding messages works. Add a new test method like this:: def test_messages(self): @@ -189,7 +184,7 @@ Running that should now give us three passing tests:: OK For more complex tests with headers and status codes, check out the -`MiniTwit Example`_ from the sources. That one contains a larger test +`MiniTwit Example`_ from the sources which contains a larger test suite. @@ -200,12 +195,12 @@ suite. Other Testing Tricks -------------------- -Besides using the test client we used above, there is also the -:meth:`~flask.Flask.test_request_context` method that in combination with -the `with` statement can be used to activate a request context -temporarily. With that you can access the :class:`~flask.request`, +Besides using the test client as shown above, there is also the +:meth:`~flask.Flask.test_request_context` method that can be used +in combination with the `with` statement to activate a request context +temporarily. With this you can access the :class:`~flask.request`, :class:`~flask.g` and :class:`~flask.session` objects like in view -functions. Here's a full example that showcases this:: +functions. Here is a full example that demonstrates this approach:: app = flask.Flask(__name__) @@ -213,7 +208,8 @@ functions. Here's a full example that showcases this:: assert flask.request.path == '/' assert flask.request.args['name'] == 'Peter' -All the other objects that are context bound can be used the same. +All the other objects that are context bound can be used in the same +way. If you want to test your application with different configurations and there does not seem to be a good way to do that, consider switching to @@ -225,7 +221,7 @@ Keeping the Context Around .. versionadded:: 0.4 -Sometimes it can be helpful to trigger a regular request but keep the +Sometimes it is helpful to trigger a regular request but still keep the context around for a little longer so that additional introspection can happen. With Flask 0.4 this is possible by using the :meth:`~flask.Flask.test_client` with a `with` block:: @@ -236,9 +232,10 @@ happen. With Flask 0.4 this is possible by using the rv = c.get('/?tequila=42') assert request.args['tequila'] == '42' -If you would just be using the :meth:`~flask.Flask.test_client` without +If you were to use just the :meth:`~flask.Flask.test_client` without the `with` block, the `assert` would fail with an error because `request` -is no longer available (because used outside of an actual request). -Keep in mind however that :meth:`~flask.Flask.after_request` functions -are already called at that point so your database connection and +is no longer available (because you are trying to use it outside of the actual request). +However, keep in mind that any :meth:`~flask.Flask.after_request` functions +are already called at this point so your database connection and everything involved is probably already closed down. + From fbd488678f93c13527a9fba252bd89b53abec5e9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Mar 2011 16:13:58 -0400 Subject: [PATCH 0556/3747] Implemented flask.has_request_context() --- CHANGES | 1 + docs/api.rst | 2 ++ flask/__init__.py | 1 + flask/ctx.py | 32 ++++++++++++++++++++++++++++++++ tests/flask_tests.py | 12 ++++++++++++ 5 files changed, 48 insertions(+) diff --git a/CHANGES b/CHANGES index 32329ef8..9420dac6 100644 --- a/CHANGES +++ b/CHANGES @@ -39,6 +39,7 @@ Release date to be announced, codename to be selected - Implemented a silent flag for `config.from_pyfile`. - Added `teardown_request` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. +- Implemented :func:`flask.has_request_context` Version 0.6.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index fb604abf..c2d90ce6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -224,6 +224,8 @@ Useful Functions and Classes This is a proxy. See :ref:`notes-on-proxies` for more information. +.. autofunction:: has_request_context + .. autofunction:: url_for .. function:: abort(code) diff --git a/flask/__init__.py b/flask/__init__.py index ee8508bc..3a232e5e 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -21,6 +21,7 @@ from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ get_template_attribute, make_response from .globals import current_app, g, request, session, _request_ctx_stack +from .ctx import has_request_context from .module import Module from .templating import render_template, render_template_string from .session import Session diff --git a/flask/ctx.py b/flask/ctx.py index d3ac231d..b63f09f1 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -19,6 +19,38 @@ class _RequestGlobals(object): pass +def has_request_context(): + """If you have code that wants to test if a request context is there or + not this function can be used. For instance if you want to take advantage + of request information is it's available but fail silently if the request + object is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g` for truthness):: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _request_ctx_stack.top is not None + + class _RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e3631202..2283b0c2 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -89,6 +89,18 @@ class ContextTestCase(unittest.TestCase): assert meh() == 'http://localhost/meh' assert flask._request_ctx_stack.top is None + def test_context_test(self): + app = flask.Flask(__name__) + assert not flask.request + assert not flask.has_request_context() + ctx = app.test_request_context() + ctx.push() + try: + assert flask.request + assert flask.has_request_context() + finally: + ctx.pop() + def test_manual_context_binding(self): app = flask.Flask(__name__) @app.route('/') From 60de3f295b0dc90635998772e7651b09aa75fab1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 14 Mar 2011 16:44:58 -0400 Subject: [PATCH 0557/3747] Updated appdispatch pattern --- docs/patterns/appdispatch.rst | 112 ++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 91858450..7849b2ef 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -3,6 +3,43 @@ Application Dispatching ======================= +Application dispatching is the process of combining multiple Flask +applications on the WSGI level. You can not only combine Flask +applications into something larger but any WSGI application. This would +even allow you to run a Django and a Flask application in the same +interpreter side by side if you want. The usefulness of this depends on +how the applications work internally. + +This is fundamentally different from the :ref:`module approach +` is that in this case you are running the same or +different Flask applications that are entirely isolated from each other. +They run different configurations and are dispatched on the WSGI level. + +Combining Applications +---------------------- + +If you have entirely separated applications and you want them to work next +to each other in the same Python interpreter process you can take +advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea +here is that each Flask application is a valid WSGI application and they +are combined by the dispatcher middleware into a larger one that +dispatched based on prefix. + +For example you could have your main application run on `/` and your +backend interface on `/admin`:: + + from werkzeug.wsgi import DispatcherMiddleware + from frontend_app import application as frontend + from backend_app import application as backend + + application = DispatcherMiddleware(frontend, { + '/backend': backend + }) + + +Dispatch by Subdomain +--------------------- + Sometimes you might want to use multiple instances of the same application with different configurations. Assuming the application is created inside a function and you can call that function to instanciate it, that is @@ -10,21 +47,17 @@ really easy to implement. In order to develop your application to support creating new instances in functions have a look at the :ref:`app-factories` pattern. - -Dispatch by Subdomain ---------------------- - A very common example would be creating applications per subdomain. For instance you configure your webserver to dispatch all requests for all subdomains to your application and you then use the subdomain information -to create user-specific instances. +to create user-specific instances. Once you have your server set up to +listen on all subdomains you can use a very simple WSGI application to do +the dynamic application creation. -Once you have your server set up to listen on all subdomains you can use a -very simple WSGI application to do the dynamic application creation. - -The code for the dispatching looks roughly like this: - -.. sourcecode:: python +The perfect level for abstraction in that regard is the WSGI layer. You +write your own WSGI application that looks at the request that comes and +and delegates it to your Flask application. If that application does not +exist yet, it is dynamically created and remembered:: from threading import Lock @@ -43,21 +76,16 @@ The code for the dispatching looks roughly like this: with self.lock: app = self.instances.get(subdomain) if app is None: - app = self.make_app(subdomain) + app = self.create_app(subdomain) self.instances[subdomain] = app return app - def make_app(self, subdomain): - return self.create_app(subdomain) - def __call__(self, environ, start_response): app = self.get_application(environ['HTTP_HOST']) return app(environ, start_response) -If you want to use it, you can do something like this: - -.. sourcecode:: python +This dispatcher can then be used like this:: from myapplication import create_app, get_user_for_subdomain from werkzeug.exceptions import NotFound @@ -76,3 +104,51 @@ If you want to use it, you can do something like this: return create_app(user) application = SubdomainDispatcher('example.com', make_app) + + +Dispatch by Path +---------------- + +Dispatching by a path on the URL is very similar. Instead of looking at +the `Host` header to figure out the subdomain one simply looks at the +request path up to the first slash:: + + from threading import Lock + from werkzeug.wsgi import pop_path_info, peek_path_info + + class PathDispatcher(object): + + def __init__(self, default_app, create_app): + self.default_app = default_app + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, prefix): + with self.lock: + app = self.instances.get(prefix) + if app is None: + app = self.create_app(prefix) + if app is not None: + self.instances[prefix] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(peek_path_info(environ)) + if app is not None: + pop_path_info(environ) + else: + app = self.default_app + return app(environ, start_response) + +The big difference between this and the subdomain one is that this one +falls back to another application if the creator function returns `None`:: + + from myapplication import create_app, default_app, get_user_for_prefix + + def make_app(prefix): + user = get_user_for_prefix(prefix) + if user is not None: + return create_app(user) + + application = PathDispatcher('example.com', default_app, make_app) From c1c20ac108bab9daea0c4741e19a68a2186db6e3 Mon Sep 17 00:00:00 2001 From: Matt Dawson Date: Mon, 14 Mar 2011 19:23:56 -0400 Subject: [PATCH 0558/3747] Modify extensionsdev documentation. --- docs/extensiondev.rst | 125 +++++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index e468d8a1..a5cc2aa4 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -137,7 +137,6 @@ Now this is where your extension code goes. But how exactly should such an extension look like? What are the best practices? Continue reading for some insight. - Initializing Extensions ----------------------- @@ -165,8 +164,8 @@ classes: a remote application that uses OAuth. What to use depends on what you have in mind. For the SQLite 3 extension -we will need to use the class based approach because we have to use a -controller object that can be used to connect to the database. +we will use the class based approach because it will provide users with a +manager object that handles opening and closing database connections. The Extension Code ------------------ @@ -175,87 +174,124 @@ Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: from __future__ import absolute_import import sqlite3 - from flask import g + + from flask import _request_ctx_stack class SQLite3(object): - + def __init__(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - - self.app.before_request(self.before_request) self.app.after_request(self.after_request) + self.app.before_request(self.before_request) def connect(self): return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) def before_request(self): - g.sqlite3_db = self.connect() + ctx = _request_ctx_stack.top + ctx.sqlite3_db = self.connect() def after_request(self, response): - g.sqlite3_db.close() + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() return response -So here's what the lines of code do: + def get_db(self): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.sqlite3_db -1. the ``__future__`` import is necessary to activate absolute imports. - This is needed because otherwise we could not call our module - `sqlite3.py` and import the top-level `sqlite3` module which actually - implements the connection to SQLite. -2. We create a class for our extension that sets a default configuration - for the SQLite 3 database if it's not there (:meth:`dict.setdefault`) - and connects two functions as before and after request handlers. -3. Then it implements a `connect` function that returns a new database - connection and the two handlers. +So here's what these lines of code do: -So why did we decide on a class based approach here? Because using that +1. The ``__future__`` import is necessary to activate absolute imports. + Otherwise we could not call our module `sqlite3.py` and import the + top-level `sqlite3` module which actually implements the connection to + SQLite. +2. We create a class for our extension that requires a supplied `app` object, + sets a configuration for the database if it's not there + (:meth:`dict.setdefault`), and attaches `before_request` and + `after_request` handlers. +3. Next, we define a `connect` function that opens a database connection. +4. Then we set up the request handlers we bound to the app above. Note here + that we're attaching our database connection to the top request context via + `_request_ctx_stack.top`. Extensions should use the top context and not the + `g` object to store things like database connections. +5. Finally, we add a `get_db` function that simplifies access to the context's + database. + +So why did we decide on a class based approach here? Because using our extension looks something like this:: - from flask import Flask, g + from flask import Flask from flaskext.sqlite3 import SQLite3 app = Flask(__name__) app.config.from_pyfile('the-config.cfg') - db = SQLite(app) + manager = SQLite3(app) + db = manager.get_db() -Either way you can use the database from the views like this:: +You can then use the database from views like this:: @app.route('/') def show_all(): - cur = g.sqlite3_db.cursor() + cur = db.cursor() cur.execute(...) -But how would you open a database connection from outside a view function? -This is where the `db` object now comes into play: +Opening a database connection from outside a view function is simple. >>> from yourapplication import db ->>> con = db.connect() ->>> cur = con.cursor() +>>> cur = db.cursor() +>>> cur.execute(...) -If you don't need that, you can go with initialization functions. +Adding an `init_app` Function +----------------------------- -Initialization Functions ------------------------- +In practice, you'll almost always want to permit users to initialize your +extension and provide an app object after the fact. This can help avoid +circular import problems when a user is breaking their app into multiple files. +Our extension could add an `init_app` function as follows:: -Here's what the module would look like with initialization functions:: + class SQLite3(object): - from __future__ import absolute_import - import sqlite3 - from flask import g + def __init__(self, app=None): + if app is not None: + self.app = app + self.init_app(self.app) + else: + self.app = None - def init_sqlite3(app): - app = app - app.config.setdefault('SQLITE3_DATABASE', ':memory:') + def init_app(self, app): + self.app = app + self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') + self.app.after_request(self.after_request) + self.app.before_request(self.before_request) - @app.before_request - def before_request(): - g.sqlite3_db = sqlite3.connect(self.app.config['SQLITE3_DATABASE']) + def connect(self): + return sqlite3.connect(app.config['SQLITE3_DATABASE']) - @app.after_request - def after_request(response): - g.sqlite3_db.close() + def before_request(self): + ctx = _request_ctx_stack.top + ctx.sqlite3_db = self.connect() + + def after_request(self, response): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() return response + def get_db(self): + ctx = _request_ctx_stack.top + if ctx is not None: + return ctx.sqlite3_db + +The user could then initialize the extension in one file:: + + manager = SQLite3() + +and bind their app to the extension in another file:: + + manager.init_app(app) + Learn from Others ----------------- @@ -276,7 +312,6 @@ designing the API. The best Flask extensions are extensions that share common idioms for the API. And this can only work if collaboration happens early. - Approved Extensions ------------------- From 1a7f579ece2528e73d9959d6fe4c8d7172fc3959 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Mon, 14 Mar 2011 23:32:33 -0400 Subject: [PATCH 0559/3747] Improved botched docstring wording for silent failure. --- flask/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/config.py b/flask/config.py index b276f168..b588dfa1 100644 --- a/flask/config.py +++ b/flask/config.py @@ -89,7 +89,7 @@ class Config(dict): app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. :return: bool. `True` if able to load config, `False` otherwise. """ @@ -113,7 +113,7 @@ class Config(dict): :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the root path. - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. .. versionadded:: 0.7 From 43cf3f307fc872b37de8b8fe986d4af17f767d63 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 15 Mar 2011 11:25:17 -0400 Subject: [PATCH 0560/3747] Improved wording --- docs/patterns/appdispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 7849b2ef..2f8be093 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -10,7 +10,7 @@ even allow you to run a Django and a Flask application in the same interpreter side by side if you want. The usefulness of this depends on how the applications work internally. -This is fundamentally different from the :ref:`module approach +The fundamental difference from the :ref:`module approach ` is that in this case you are running the same or different Flask applications that are entirely isolated from each other. They run different configurations and are dispatched on the WSGI level. From 97efffad9a9858c0d966723d543e1186629631d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 15 Mar 2011 11:47:59 -0400 Subject: [PATCH 0561/3747] Enable deprecation warnings --- tests/flask_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 04b54777..46455e99 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -47,6 +47,10 @@ SECRET_KEY = 'devkey' def catch_warnings(): """Catch warnings in a with block in a list""" import warnings + + # make sure deprecation warnings are active in tests + warnings.simplefilter('default', category=DeprecationWarning) + filters = warnings.filters warnings.filters = filters[:] old_showwarning = warnings.showwarning From 67581795c4b1d5d27aaadf71f82bb6862887ba74 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Mon, 14 Mar 2011 23:32:33 -0400 Subject: [PATCH 0562/3747] Improved botched docstring wording for silent failure. Signed-off-by: Armin Ronacher --- flask/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/config.py b/flask/config.py index b276f168..b588dfa1 100644 --- a/flask/config.py +++ b/flask/config.py @@ -89,7 +89,7 @@ class Config(dict): app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) :param variable_name: name of the environment variable - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. :return: bool. `True` if able to load config, `False` otherwise. """ @@ -113,7 +113,7 @@ class Config(dict): :param filename: the filename of the config. This can either be an absolute filename or a filename relative to the root path. - :param silent: set to `True` if you want silent to fail for missing + :param silent: set to `True` if you want silent failure for missing files. .. versionadded:: 0.7 From 4c8c503326f8b26787945082007a294dff4c255b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Mar 2011 09:00:24 +0100 Subject: [PATCH 0563/3747] break line --- flask/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index fad49945..49c8fe10 100644 --- a/flask/app.py +++ b/flask/app.py @@ -718,7 +718,9 @@ class Flask(_PackageBoundObject): return f def teardown_request(self, f): - """Register a function to be run at the end of each request, regardless of whether there was an exception or not.""" + """Register a function to be run at the end of each request, + regardless of whether there was an exception or not. + """ self.teardown_request_funcs.setdefault(None, []).append(f) return f From 0da56d7f5cfb6a637129444bfe0d6bf2584363ac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Mar 2011 09:15:28 +0100 Subject: [PATCH 0564/3747] deprecated init_jinja_globals --- CHANGES | 3 +++ flask/app.py | 50 ++++++++++++++++++++++++++++++-------------- flask/helpers.py | 31 +++++++++++++++++++++++++++ tests/flask_tests.py | 20 ++++++++++++++++++ 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index 9420dac6..394cd39e 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,9 @@ Release date to be announced, codename to be selected - Added `teardown_request` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` +- Deprecated `init_jinja_globals`. Override the + :meth:`~flask.Flask.create_jinja_environment` method instead to + achieve the same functionality. Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index 49c8fe10..706ef229 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,7 +24,7 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, _endpoint_from_view_func + locked_cached_property, _tojson_filter, _endpoint_from_view_func from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import _RequestContext @@ -317,11 +317,6 @@ class Flask(_PackageBoundObject): endpoint='static', view_func=self.send_static_file) - #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options`. - self.jinja_env = self.create_jinja_environment() - self.init_jinja_globals() - @property def propagate_exceptions(self): """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration @@ -356,16 +351,43 @@ class Flask(_PackageBoundObject): self._logger = rv = create_logger(self) return rv + @locked_cached_property + def jinja_env(self): + """The Jinja2 environment used to load templates.""" + rv = self.create_jinja_environment() + + # Hack to support the init_jinja_globals method which is supported + # until 1.0 but has an API deficiency. + if getattr(self.init_jinja_globals, 'im_func', None) is not \ + Flask.init_jinja_globals.im_func: + from warnings import warn + warn(DeprecationWarning('This flask class uses a customized ' + 'init_jinja_globals() method which is deprecated. ' + 'Move the code from that method into the ' + 'create_jinja_environment() method instead.')) + self.__dict__['jinja_env'] = rv + self.init_jinja_globals() + + return rv + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` - and :meth:`select_jinja_autoescape`. + and :meth:`select_jinja_autoescape`. Since 0.7 this also adds + the Jinja2 globals and filters after initialization. Override + this function to customize the behavior. .. versionadded:: 0.5 """ options = dict(self.jinja_options) if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape - return Environment(loader=self.create_jinja_loader(), **options) + rv = Environment(loader=self.create_jinja_loader(), **options) + rv.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages + ) + rv.filters['tojson'] = _tojson_filter + return rv def create_jinja_loader(self): """Creates the loader for the Jinja2 environment. Can be used to @@ -376,17 +398,13 @@ class Flask(_PackageBoundObject): return _DispatchingJinjaLoader(self) def init_jinja_globals(self): - """Called directly after the environment was created to inject - some defaults (like `url_for`, `get_flashed_messages` and the - `tojson` filter. + """Deprecated. Used to initialize the Jinja2 globals. .. versionadded:: 0.5 + .. versionchanged:: 0.7 + This method is deprecated with 0.7. Override + :meth:`create_jinja_environment` instead. """ - self.jinja_env.globals.update( - url_for=url_for, - get_flashed_messages=get_flashed_messages - ) - self.jinja_env.filters['tojson'] = _tojson_filter def select_jinja_autoescape(self, filename): """Returns `True` if autoescaping should be active for the given diff --git a/flask/helpers.py b/flask/helpers.py index ed8a5d5c..7539b248 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -15,6 +15,7 @@ import posixpath import mimetypes from time import time from zlib import adler32 +from threading import RLock # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -58,6 +59,10 @@ else: _tojson_filter = json.dumps +# sentinel +_missing = object() + + # what separators does this operating system provide that are not a slash? # this is used by the send_from_directory function to ensure that nobody is # able to access files from outside the filesystem. @@ -435,6 +440,32 @@ def _get_package_path(name): return os.getcwd() +class locked_cached_property(object): + """A decorator that converts a function into a lazy property. The + function wrapped is called the first time to retrieve the result + and then that calculated result is used the next time you access + the value. Works like the one in Werkzeug but has a lock for + thread safety. + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + self.lock = RLock() + + def __get__(self, obj, type=None): + if obj is None: + return self + with self.lock: + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + class _PackageBoundObject(object): def __init__(self, import_name): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 19168180..a9ad9464 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1564,6 +1564,25 @@ class TestSignals(unittest.TestCase): flask.got_request_exception.disconnect(record, app) +class DeprecationsTestCase(unittest.TestCase): + + def test_init_jinja_globals(self): + class MyFlask(flask.Flask): + def init_jinja_globals(self): + self.jinja_env.globals['foo'] = '42' + + with catch_warnings() as log: + app = MyFlask(__name__) + @app.route('/') + def foo(): + return app.jinja_env.globals['foo'] + + c = app.test_client() + assert c.get('/').data == '42' + assert len(log) == 1 + assert 'init_jinja_globals' in str(log[0]['message']) + + def suite(): from minitwit_tests import MiniTwitTestCase from flaskr_tests import FlaskrTestCase @@ -1576,6 +1595,7 @@ def suite(): suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) suite.addTest(unittest.makeSuite(SubdomainTestCase)) + suite.addTest(unittest.makeSuite(DeprecationsTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) if flask.signals_available: From 5910ef8a439196075f16e8d19d72c8bcb704a62f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Mar 2011 09:16:33 +0100 Subject: [PATCH 0565/3747] Lock loader creation --- flask/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 7539b248..3f58b454 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -34,7 +34,7 @@ except ImportError: json_available = False -from werkzeug import Headers, wrap_file, cached_property +from werkzeug import Headers, wrap_file from werkzeug.exceptions import NotFound from jinja2 import FileSystemLoader @@ -485,7 +485,7 @@ class _PackageBoundObject(object): """ return os.path.isdir(os.path.join(self.root_path, 'static')) - @cached_property + @locked_cached_property def jinja_loader(self): """The Jinja loader for this package bound object. From 1446614915b9ab4f75cbdfd79a80b71492b00276 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 18 Mar 2011 09:30:56 +0100 Subject: [PATCH 0566/3747] Added deprecation warnings for modules --- flask/app.py | 17 +++++++++++++++++ tests/flask_tests.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/flask/app.py b/flask/app.py index 706ef229..2bb722d4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -169,6 +169,11 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.4 logger_name = ConfigAttribute('LOGGER_NAME') + #: Enable the deprecated module support? This is active by default + #: in 0.7 but will be changed to False in 0.8. With Flask 1.0 modules + #: will be removed in favor of Blueprints + enable_modules = True + #: The logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging #: handler does the formatting. @@ -533,6 +538,18 @@ class Flask(_PackageBoundObject): :class:`Module` class and will override the values of the module if provided. """ + if not self.enable_modules: + raise RuntimeError('Module support was disabled but code ' + 'attempted to register a module named %r' % module) + else: + from warnings import warn + warn(DeprecationWarning('Modules are deprecated. Upgrade to ' + 'using blueprints. Have a look into the documentation for ' + 'more information. If this module was registered by a ' + 'Flask-Extension upgrade the extension or contact the author ' + 'of that extension instead. (Registered %r)' % module), + stacklevel=2) + options.setdefault('url_prefix', module.url_prefix) options.setdefault('subdomain', module.subdomain) self.view_functions.update(module.view_functions) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index a9ad9464..73b7731d 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -15,9 +15,11 @@ import re import sys import flask import unittest +import warnings from threading import Thread from logging import StreamHandler from contextlib import contextmanager +from functools import update_wrapper from datetime import datetime from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound @@ -43,11 +45,16 @@ TEST_KEY = 'foo' SECRET_KEY = 'devkey' +# import moduleapp here because it uses deprecated features and we don't +# want to see the warnings +warnings.simplefilter('ignore', DeprecationWarning) +from moduleapp import app as moduleapp +warnings.simplefilter('default', DeprecationWarning) + + @contextmanager def catch_warnings(): """Catch warnings in a with block in a list""" - import warnings - # make sure deprecation warnings are active in tests warnings.simplefilter('default', category=DeprecationWarning) @@ -76,6 +83,16 @@ def catch_stderr(): sys.stderr = old_stderr +def emits_module_deprecation_warning(f): + def new_f(*args, **kwargs): + with catch_warnings() as log: + f(*args, **kwargs) + assert log, 'expected deprecation warning' + for entry in log: + assert 'Modules are deprecated' in str(entry['message']) + return update_wrapper(new_f, f) + + class ContextTestCase(unittest.TestCase): def test_context_binding(self): @@ -961,6 +978,7 @@ class TemplatingTestCase(unittest.TestCase): class ModuleTestCase(unittest.TestCase): + @emits_module_deprecation_warning def test_basic_module(self): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin', url_prefix='/admin') @@ -983,6 +1001,7 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/admin/login').data == 'admin login' assert c.get('/admin/logout').data == 'admin logout' + @emits_module_deprecation_warning def test_default_endpoint_name(self): app = flask.Flask(__name__) mod = flask.Module(__name__, 'frontend') @@ -995,6 +1014,7 @@ class ModuleTestCase(unittest.TestCase): with app.test_request_context(): assert flask.url_for('frontend.index') == '/' + @emits_module_deprecation_warning def test_request_processing(self): catched = [] app = flask.Flask(__name__) @@ -1030,6 +1050,7 @@ class ModuleTestCase(unittest.TestCase): assert catched == ['before-app', 'before-admin', 'after-admin', 'after-app'] + @emits_module_deprecation_warning def test_context_processors(self): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin', url_prefix='/admin') @@ -1053,6 +1074,7 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/').data == '13' assert c.get('/admin/').data == '123' + @emits_module_deprecation_warning def test_late_binding(self): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin') @@ -1062,6 +1084,7 @@ class ModuleTestCase(unittest.TestCase): app.register_module(admin, url_prefix='/admin') assert app.test_client().get('/admin/').data == '42' + @emits_module_deprecation_warning def test_error_handling(self): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin') @@ -1087,7 +1110,7 @@ class ModuleTestCase(unittest.TestCase): assert 'internal server error' == rv.data def test_templates_and_static(self): - from moduleapp import app + app = moduleapp c = app.test_client() rv = c.get('/') @@ -1117,7 +1140,7 @@ class ModuleTestCase(unittest.TestCase): assert flask.render_template('nested/nested.txt') == 'I\'m nested' def test_safe_access(self): - from moduleapp import app + app = moduleapp with app.test_request_context(): f = app.view_functions['admin.static'] @@ -1150,6 +1173,7 @@ class ModuleTestCase(unittest.TestCase): finally: os.path = old_path + @emits_module_deprecation_warning def test_endpoint_decorator(self): from werkzeug.routing import Submount, Rule from flask import Module @@ -1439,6 +1463,7 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://test.localhost/') assert rv.data == 'test index' + @emits_module_deprecation_warning def test_module_static_path_subdomain(self): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'example.com' @@ -1459,6 +1484,7 @@ class SubdomainTestCase(unittest.TestCase): rv = c.get('/', 'http://mitsuhiko.localhost/') assert rv.data == 'index for mitsuhiko' + @emits_module_deprecation_warning def test_module_subdomain_support(self): app = flask.Flask(__name__) mod = flask.Module(__name__, 'test', subdomain='testing') From a06cd0a64418f2aafafb0a574c64c2ea5f3e9239 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 19 Mar 2011 03:28:39 +0100 Subject: [PATCH 0567/3747] Started work on implementing blueprint based template loading --- flask/app.py | 32 +++++++++++++++++--- flask/templating.py | 72 ++++++++++++++++++++++++++++++++++---------- flask/wrappers.py | 10 +++++- tests/flask_tests.py | 1 + 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/flask/app.py b/flask/app.py index 2bb722d4..243ae4a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -16,8 +16,6 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment - from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, \ @@ -31,7 +29,7 @@ from .ctx import _RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState -from .templating import _DispatchingJinjaLoader, \ +from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception @@ -280,6 +278,13 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.5 self.modules = {} + #: all the attached blueprints in a directory by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. + #: + #: .. versionadded:: 0.7 + self.blueprints = {} + #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and #: similar things. For backwards compatibility extensions should register @@ -386,7 +391,7 @@ class Flask(_PackageBoundObject): options = dict(self.jinja_options) if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape - rv = Environment(loader=self.create_jinja_loader(), **options) + rv = Environment(self, **options) rv.globals.update( url_for=url_for, get_flashed_messages=get_flashed_messages @@ -400,7 +405,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - return _DispatchingJinjaLoader(self) + return DispatchingJinjaLoader(self) def init_jinja_globals(self): """Deprecated. Used to initialize the Jinja2 globals. @@ -537,6 +542,10 @@ class Flask(_PackageBoundObject): of this function are the same as the ones for the constructor of the :class:`Module` class and will override the values of the module if provided. + + .. versionchanged:: 0.7 + The module system was deprecated in favor for the blueprint + system. """ if not self.enable_modules: raise RuntimeError('Module support was disabled but code ' @@ -557,6 +566,19 @@ class Flask(_PackageBoundObject): for func in module._register_events: func(state) + def register_blueprint(self, blueprint, **options): + """Registers a blueprint on the application. + + .. versionadded:: 0.7 + """ + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint\'s name collision ocurred between %r and ' \ + '%r.' % (blueprint, self.blueprints[blueprint.name]) + else: + self.blueprints[blueprint.name] = blueprint + blueprint.register(self, **options) + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the diff --git a/flask/templating.py b/flask/templating.py index 4db03b75..8e785169 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -9,7 +9,8 @@ :license: BSD, see LICENSE for more details. """ import posixpath -from jinja2 import BaseLoader, TemplateNotFound +from jinja2 import BaseLoader, Environment as BaseEnvironment, \ + TemplateNotFound from .globals import _request_ctx_stack from .signals import template_rendered @@ -28,7 +29,25 @@ def _default_template_ctx_processor(): ) -class _DispatchingJinjaLoader(BaseLoader): +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app, **options): + if 'loader' not in options: + options['loader'] = app.create_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + def join_path(self, template, parent): + if template and template[0] == ':': + template = parent.split(':', 1)[0] + template + return template + + +class DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all the module folders. """ @@ -37,31 +56,50 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): - template = posixpath.normpath(template) - if template.startswith('../'): - raise TemplateNotFound(template) + # newstyle template support. blueprints are explicit and no further + # magic is involved. If the template cannot be loaded by the + # blueprint loader it just gives up, no further steps involved. + if ':' in template: + blueprint_name, local_template = template.split(':', 1) + local_template = posixpath.normpath(local_template) + blueprint = self.app.blueprints.get(blueprint_name) + if blueprint is None: + raise TemplateNotFound(template) + loader = blueprint.jinja_loader + if loader is not None: + return loader.get_source(environment, local_template) + + # if modules are enabled we call into the old style template lookup + # and try that before we go with the real deal. loader = None try: - module, name = template.split('/', 1) + module, name = posixpath.normpath(template).split('/', 1) loader = self.app.modules[module].jinja_loader - except (ValueError, KeyError): + except (ValueError, KeyError, TemplateNotFound): pass - # if there was a module and it has a loader, try this first - if loader is not None: - try: + try: + if loader is not None: return loader.get_source(environment, name) - except TemplateNotFound: - pass - # fall back to application loader if module failed + except TemplateNotFound: + pass + + # at the very last, load templates from the environment return self.app.jinja_loader.get_source(environment, template) def list_templates(self): - result = self.app.jinja_loader.list_templates() + result = set(self.app.jinja_loader.list_templates()) + for name, module in self.app.modules.iteritems(): if module.jinja_loader is not None: for template in module.jinja_loader.list_templates(): - result.append('%s/%s' % (name, template)) - return result + result.add('%s/%s' % (name, template)) + + for name, blueprint in self.app.blueprints.iteritems(): + if blueprint.jinja_loader is not None: + for template in blueprint.jinja_loader.list_templates(): + result.add('%s:%s' % (name, template)) + + return list(result) def _render(template, context, app): @@ -81,6 +119,8 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) + if template_name[:1] == ':': + template_name = ctx.request.blueprint + template_name return _render(ctx.app.jinja_env.get_template(template_name), context, ctx.app) diff --git a/flask/wrappers.py b/flask/wrappers.py index 4db1e782..422085a0 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -62,9 +62,17 @@ class Request(RequestBase): @property def module(self): """The name of the current module""" - if self.url_rule and '.' in self.url_rule.endpoint: + if self.url_rule and \ + ':' not in self.url_rule.endpoint and \ + '.' in self.url_rule.endpoint: return self.url_rule.endpoint.rsplit('.', 1)[0] + @property + def blueprint(self): + """The name of the current blueprint""" + if self.url_rule and ':' in self.url_rule.endpoint: + return self.url_rule.endpoint.split(':', 1)[0] + @cached_property def json(self): """If the mimetype is `application/json` this will contain the diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 73b7731d..f6454a88 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1111,6 +1111,7 @@ class ModuleTestCase(unittest.TestCase): def test_templates_and_static(self): app = moduleapp + app.debug = True c = app.test_client() rv = c.get('/') From 0ecc686372f577ccbcbcb818e1386ceb7bd9391d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 17 Apr 2011 19:03:06 +0200 Subject: [PATCH 0568/3747] Fixed a typo --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index ed8a5d5c..458d6252 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -321,7 +321,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if not attachment_filename and not mimetype \ and isinstance(filename, basestring): warn(DeprecationWarning('The filename support for file objects ' - 'passed to send_file is not deprecated. Pass an ' + 'passed to send_file is now deprecated. Pass an ' 'attach_filename if you want mimetypes to be guessed.'), stacklevel=2) if add_etags: From a2225bf57e7c20f5c63c3c2b79db397f3724c1dc Mon Sep 17 00:00:00 2001 From: Noufal Ibrahim Date: Mon, 28 Mar 2011 17:01:21 +0530 Subject: [PATCH 0569/3747] Added a note on actually starting the application Signed-off-by: Armin Ronacher --- docs/tutorial/setup.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 64bf3b66..e9e4d679 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -70,7 +70,14 @@ server if we want to run that file as a standalone application:: app.run() With that out of the way you should be able to start up the application -without problems. When you head over to the server you will get an 404 +without problems. Do this with the following command:: + + python flaskr.py + +You will see a message telling you that server has started along with +the address at which you can access it. + +When you head over to the server in your browser you will get an 404 page not found error because we don't have any views yet. But we will focus on that a little later. First we should get the database working. From 709ecefee1859cf16cc8bdfa642e39fbc6b4421e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Roy?= Date: Wed, 30 Mar 2011 22:58:41 -0400 Subject: [PATCH 0570/3747] Updating documentation for app.after_request decorator. Signed-off-by: Armin Ronacher --- flask/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 49c8fe10..20fa42d9 100644 --- a/flask/app.py +++ b/flask/app.py @@ -713,7 +713,10 @@ class Flask(_PackageBoundObject): return f def after_request(self, f): - """Register a function to be run after each request.""" + """Register a function to be run after each request. Your function + must take one parameter, a :attr:`response_class` object and return + a new response object or the same (see :meth:`process_response`). + """ self.after_request_funcs.setdefault(None, []).append(f) return f From 0e4cd2e6512c66ba004df0f35f3a21e8cfcdf77c Mon Sep 17 00:00:00 2001 From: Kanak Kshetri Date: Thu, 31 Mar 2011 15:53:17 -0400 Subject: [PATCH 0571/3747] Fixed a typo that was preventing second code block from appearing in a code block Signed-off-by: Armin Ronacher --- docs/tutorial/dbcon.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index f700a329..b2a626f9 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -24,7 +24,7 @@ db connection in the interactive debugger:: return response If you want to guarantee that the connection is always closed in debug mode, you -can close it in a function decorated with :meth:`~flask.Flask.teardown_request`: +can close it in a function decorated with :meth:`~flask.Flask.teardown_request`:: @app.before_request def before_request(): From 7ed3196e8d1861acd027c409c47e51cce812ba63 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 4 Apr 2011 16:53:04 +0200 Subject: [PATCH 0572/3747] Add safe_join: returns the filename used by send_from_directory. Signed-off-by: Armin Ronacher --- docs/api.rst | 2 ++ flask/__init__.py | 2 +- flask/helpers.py | 34 +++++++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c2d90ce6..88d026ed 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -244,6 +244,8 @@ Useful Functions and Classes .. autofunction:: send_from_directory +.. autofunction:: safe_join + .. autofunction:: escape .. autoclass:: Markup diff --git a/flask/__init__.py b/flask/__init__.py index 3a232e5e..1274e766 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -19,7 +19,7 @@ from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute, make_response + get_template_attribute, make_response, safe_join from .globals import current_app, g, request, session, _request_ctx_stack from .ctx import has_request_context from .module import Module diff --git a/flask/helpers.py b/flask/helpers.py index 458d6252..9d9af4bf 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -388,6 +388,32 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, return rv +def safe_join(directory, filename): + """Safely join `directory` and `filename`. + + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path + would fall out of `directory`. + + Example usage:: + + @app.route('/wiki/') + def wiki_page(filename): + filename = safe_join(app.config['WIKI_FOLDER'], filename) + with open(filename, 'rb') as fd: + content = fd.read() # Read and process the file content... + + """ + filename = posixpath.normpath(filename) + for sep in _os_alt_seps: + if sep in filename: + raise NotFound() + if os.path.isabs(filename) or filename.startswith('../'): + raise NotFound() + return os.path.join(directory, filename) + + def send_from_directory(directory, filename, **options): """Send a file from a given directory with :func:`send_file`. This is a secure way to quickly expose static files from an upload folder @@ -415,13 +441,7 @@ def send_from_directory(directory, filename, **options): :param options: optional keyword arguments that are directly forwarded to :func:`send_file`. """ - filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - raise NotFound() - if os.path.isabs(filename) or filename.startswith('../'): - raise NotFound() - filename = os.path.join(directory, filename) + filename = safe_join(directory, filename) if not os.path.isfile(filename): raise NotFound() return send_file(filename, conditional=True, **options) From 3c7b5a68f1ede9c870ae76f44e2dde6102843e54 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 16:48:40 +0200 Subject: [PATCH 0573/3747] Changelog entry --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 9420dac6..f5c70477 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,7 @@ Release date to be announced, codename to be selected - Added `teardown_request` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` +- Added :func:`safe_join` Version 0.6.1 ------------- From 1dd83964f0c1fb3c4772cf24880f5aeb3f282f66 Mon Sep 17 00:00:00 2001 From: streety Date: Mon, 18 Apr 2011 02:49:31 -0700 Subject: [PATCH 0574/3747] Change to match headers in https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/contrib/fixers.py Signed-off-by: Armin Ronacher --- docs/deploying/others.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 8f08ecd1..153fb7cf 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -72,7 +72,7 @@ problematic values in the WSGI environment usually are `REMOTE_ADDR` and but you might want to write your own WSGI middleware for specific setups. The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forward-For`:: +and the remote address from `X-Forwarded-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) From d8fcd4260e9194a03267ddb913a8b5d7cf0948d2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 23:19:59 +0200 Subject: [PATCH 0575/3747] Whitespace normalization --- flask/helpers.py | 2 +- tests/flask_tests.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 9d9af4bf..e04b7f28 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -390,7 +390,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, def safe_join(directory, filename): """Safely join `directory` and `filename`. - + :param directory: the base directory. :param filename: the untrusted filename relative to that directory. :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 19168180..265f89f1 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -642,6 +642,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): app.config.update( SERVER_NAME='localhost.localdomain:5000' ) + @app.route('/') def index(): return None From e774e3a69eeb9a811a22e73ff87267e8bf4f9027 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Apr 2011 23:20:24 +0200 Subject: [PATCH 0576/3747] Switch params and example --- flask/helpers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index e04b7f28..418c4b17 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -391,11 +391,6 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, def safe_join(directory, filename): """Safely join `directory` and `filename`. - :param directory: the base directory. - :param filename: the untrusted filename relative to that directory. - :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path - would fall out of `directory`. - Example usage:: @app.route('/wiki/') @@ -404,6 +399,10 @@ def safe_join(directory, filename): with open(filename, 'rb') as fd: content = fd.read() # Read and process the file content... + :param directory: the base directory. + :param filename: the untrusted filename relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path + would fall out of `directory`. """ filename = posixpath.normpath(filename) for sep in _os_alt_seps: From a4ea3159a27d775b53774cea2d47c733a460fb93 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 25 Apr 2011 16:01:58 +0200 Subject: [PATCH 0577/3747] Better error messages --- flask/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 243ae4a3..6a637554 100644 --- a/flask/app.py +++ b/flask/app.py @@ -574,10 +574,11 @@ class Flask(_PackageBoundObject): if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint\'s name collision ocurred between %r and ' \ - '%r.' % (blueprint, self.blueprints[blueprint.name]) + '%r. Both share the same name "%s"' % \ + (blueprint, self.blueprints[blueprint.name], blueprint.name) else: self.blueprints[blueprint.name] = blueprint - blueprint.register(self, **options) + blueprint.register(self, options) def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` From 017117778ec477205eaba6acab951951808a60a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 25 Apr 2011 16:02:35 +0200 Subject: [PATCH 0578/3747] Document that None skips in query strings. This fixes #224 --- flask/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 418c4b17..ec513d8f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -166,7 +166,8 @@ def url_for(endpoint, **values): ==================== ======================= ============================= Variable arguments that are unknown to the target endpoint are appended - to the generated URL as query arguments. + to the generated URL as query arguments. If the value of a query argument + is `None`, the whole pair is skipped. For more information, head over to the :ref:`Quickstart `. From c6e4d743a93d8999a8d95044b6d89132e25cb7d9 Mon Sep 17 00:00:00 2001 From: Aaron Kavlie Date: Wed, 27 Apr 2011 14:28:07 -0700 Subject: [PATCH 0579/3747] A couple of corrections to the example fabfile. --- docs/patterns/fabric.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index 49be85ab..dbd4f913 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -32,7 +32,7 @@ hosts. These hosts can be defined either in the fabfile or on the command line. In this case we will add them to the fabfile. This is a basic first example that has the ability to upload the current -sourcecode to the server and install it into a already existing +sourcecode to the server and install it into a pre-existing virtual environment:: from fabric.api import * @@ -53,12 +53,12 @@ virtual environment:: put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter # that directory and unzip it - run('mkdir yourapplication') + run('mkdir /tmp/yourapplication') with cd('/tmp/yourapplication'): run('tar xzf /tmp/yourapplication.tar.gz') - # now setup the package with our virtual environment's - # python interpreter - run('/var/www/yourapplication/env/bin/python setup.py install') + # now setup the package with our virtual environment's + # python interpreter + run('/var/www/yourapplication/env/bin/python setup.py install') # now that all is set up, delete the folder again run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz') # and finally touch the .wsgi file so that mod_wsgi triggers From 15c937b316b16b12df2375c706bcc93e3d37d33e Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 May 2011 16:36:51 -0400 Subject: [PATCH 0580/3747] Remove ctx.bind() from doc. It doesn't exist. --- flask/app.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/flask/app.py b/flask/app.py index 20fa42d9..1e068e41 100644 --- a/flask/app.py +++ b/flask/app.py @@ -928,15 +928,6 @@ class Flask(_PackageBoundObject): finally: ctx.pop() - The big advantage of this approach is that you can use it without - the try/finally statement in a shell for interactive testing: - - >>> ctx = app.test_request_context() - >>> ctx.bind() - >>> request.path - u'/' - >>> ctx.unbind() - .. versionchanged:: 0.3 Added support for non-with statement usage and `with` statement is now passed the ctx object. From 74514fc83795d5ca705f67333993b5c7f9d01316 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 8 May 2011 11:31:52 +0200 Subject: [PATCH 0581/3747] the import order note was meant to be bold, not cursive --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index e1b92c08..28cd70e4 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -57,7 +57,7 @@ following quick checklist: 2. all the view functions (the ones with a :meth:`~flask.Flask.route` decorator on top) have to be imported when in the `__init__.py` file. Not the object itself, but the module it is in. Import the view module - *after the application object is created*. + **after the application object is created**. Here's an example `__init__.py`:: From 8b974eb35523fde507055a27f70484133bb21b79 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 12 May 2011 12:54:56 -0400 Subject: [PATCH 0582/3747] Note to use debug=False for third-party debuggers. As requested on mailing list. http://flask.pocoo.org/mailinglist/archive/2011/5/12/using-eclipse%2Bpydev-for-debugging-flask-apps/ --- docs/quickstart.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b4f6027f..c18c2332 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -113,6 +113,11 @@ Screenshot of the debugger in action: :class: screenshot :alt: screenshot of debugger in action +.. admonition:: Working With Other Debuggers + + Some third-party debuggers, e.g. PyDev and IntelliJ, are interrupted when + ``app`` reloads. To use these debuggers, set ``app.debug = False``. + Routing ------- From 57920a5808c03d7d035f592a13a71ba627fe85da Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 17 May 2011 11:41:12 -0400 Subject: [PATCH 0583/3747] Prevent pop if flashes not in session to avoid modification to session fixes #227 --- flask/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index ec513d8f..a25dcadd 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -249,7 +249,8 @@ def get_flashed_messages(with_categories=False): """ flashes = _request_ctx_stack.top.flashes if flashes is None: - _request_ctx_stack.top.flashes = flashes = session.pop('_flashes', []) + _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ + if '_flashes' in session else [] if not with_categories: return [x[1] for x in flashes] return flashes From ea77d5e12d5148e8f42e449deac64f363c5db484 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 19 May 2011 09:14:53 -0400 Subject: [PATCH 0584/3747] Touch up docs according to user feedback. --- docs/quickstart.rst | 4 ++-- flask/config.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c18c2332..fc5ae0f1 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -115,8 +115,8 @@ Screenshot of the debugger in action: .. admonition:: Working With Other Debuggers - Some third-party debuggers, e.g. PyDev and IntelliJ, are interrupted when - ``app`` reloads. To use these debuggers, set ``app.debug = False``. + Debuggers interfere with each other. If you are using another debugger + (e.g. PyDev or IntelliJ), you may need to set ``app.debug = False``. Routing diff --git a/flask/config.py b/flask/config.py index b588dfa1..bb2d6e9e 100644 --- a/flask/config.py +++ b/flask/config.py @@ -141,8 +141,8 @@ class Config(dict): Objects are usually either modules or classes. - Just the uppercase variables in that object are stored in the config - after lowercasing. Example usage:: + Just the uppercase variables in that object are stored in the config. + Example usage:: app.config.from_object('yourapplication.default_config') from yourapplication import default_config From 41952d2b259efdd89634aef7f47afc4cef6e7190 Mon Sep 17 00:00:00 2001 From: Steve Romanow Date: Wed, 18 May 2011 18:16:48 -0400 Subject: [PATCH 0585/3747] respect request charset Signed-off-by: Armin Ronacher --- flask/wrappers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 4db1e782..557ab841 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -73,7 +73,13 @@ class Request(RequestBase): if __debug__: _assert_have_json() if self.mimetype == 'application/json': - return json.loads(self.data) + request_charset = self.mimetype_params.get('charset') + if request_charset is not None: + j = json.loads(self.data, encoding=request_charset ) + else: + j = json.loads(self.data) + + return j class Response(ResponseBase): From d90765b0265eec81130172efe3250cd1e3c900bd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:29:46 +0200 Subject: [PATCH 0586/3747] Added testcase for json encoding parameter support --- tests/flask_tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 265f89f1..6b6daaa4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -785,6 +785,18 @@ class BasicFunctionalityTestCase(unittest.TestCase): class JSONTestCase(unittest.TestCase): + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + assert resp.data == u'Hällo Wörld'.encode('utf-8') + def test_jsonify(self): d = dict(a=23, b=42, c=[1, 2, 3]) app = flask.Flask(__name__) From 2a81c8a822e166299fed27a403a59a004f689d37 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:30:08 +0200 Subject: [PATCH 0587/3747] Documented change --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index f5c70477..f7645d71 100644 --- a/CHANGES +++ b/CHANGES @@ -41,6 +41,8 @@ Release date to be announced, codename to be selected of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` - Added :func:`safe_join` +- The automatic JSON request data unpacking now looks at the charset + mimetype parameter. Version 0.6.1 ------------- From 7242abcfb22c5b7321dc658766e8b6f5486ff1c3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:34:41 +0200 Subject: [PATCH 0588/3747] Extend the logging from the dynamically set logger class. This fixes #234 --- flask/logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/logging.py b/flask/logging.py index 29caadce..8379ab66 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -11,7 +11,7 @@ from __future__ import absolute_import -from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG +from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG def create_logger(app): @@ -21,6 +21,7 @@ def create_logger(app): function also removes all attached handlers in case there was a logger with the log name before. """ + Logger = getLoggerClass() class DebugLogger(Logger): def getEffectiveLevel(x): From 34e6933832f16a2031691b7397e42d9d157bd054 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 24 May 2011 16:42:06 +0200 Subject: [PATCH 0589/3747] Documented change in flashing --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index f7645d71..e10744ca 100644 --- a/CHANGES +++ b/CHANGES @@ -43,6 +43,8 @@ Release date to be announced, codename to be selected - Added :func:`safe_join` - The automatic JSON request data unpacking now looks at the charset mimetype parameter. +- Don't modify the session on :func:`flask.get_flashed_messages` if there + are no messages in the session. Version 0.6.1 ------------- From e3f2dd8f080115d626437bbee3631f7cf5560ca5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 15:59:11 +0200 Subject: [PATCH 0590/3747] Added a test for content length behavior --- tests/flask_tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6b6daaa4..b723f217 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -782,6 +782,22 @@ class BasicFunctionalityTestCase(unittest.TestCase): t.start() t.join() + def test_max_content_length(self): + app = flask.Flask(__name__) + app.debug = True + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + assert False + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + assert rv.data == '42' + class JSONTestCase(unittest.TestCase): From e71a5ff8de93801c30ed6daecac4b8502aa86813 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:10:53 +0200 Subject: [PATCH 0591/3747] Started work on new request dispatching. Unittests not yet updated --- docs/api.rst | 51 ++-------- docs/config.rst | 53 ++++++---- docs/contents.rst.inc | 1 + docs/reqcontext.rst | 230 ++++++++++++++++++++++++++++++++++++++++++ docs/shell.rst | 65 +++++------- docs/signals.rst | 16 +++ flask/__init__.py | 2 +- flask/app.py | 130 ++++++++++++++++-------- flask/ctx.py | 35 ++++++- flask/signals.py | 1 + tests/flask_tests.py | 5 +- 11 files changed, 436 insertions(+), 153 deletions(-) create mode 100644 docs/reqcontext.rst diff --git a/docs/api.rst b/docs/api.rst index 88d026ed..b3953537 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -310,6 +310,9 @@ Configuration Useful Internals ---------------- +.. autoclass:: flask.ctx.RequestContext + :members: + .. data:: _request_ctx_stack The internal :class:`~werkzeug.local.LocalStack` that is used to implement @@ -347,23 +350,6 @@ Useful Internals if ctx is not None: return ctx.session - .. versionchanged:: 0.4 - - The request context is automatically popped at the end of the request - for you. In debug mode the request context is kept around if - exceptions happen so that interactive debuggers have a chance to - introspect the data. With 0.4 this can also be forced for requests - that did not fail and outside of `DEBUG` mode. By setting - ``'flask._preserve_context'`` to `True` on the WSGI environment the - context will not pop itself at the end of the request. This is used by - the :meth:`~flask.Flask.test_client` for example to implement the - deferred cleanup functionality. - - You might find this helpful for unittests where you need the - information from the context local around for a little longer. Make - sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in - that situation, otherwise your unittests will leak memory. - Signals ------- @@ -401,6 +387,12 @@ Signals in debug mode, where no exception handling happens. The exception itself is passed to the subscriber as `exception`. +.. data:: request_tearing_down + + This signal is sent when the application is tearing down the request. + This is always called, even if an error happened. No arguments are + provided. + .. currentmodule:: None .. class:: flask.signals.Namespace @@ -418,28 +410,3 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker - -.. _notes-on-proxies: - -Notes On Proxies ----------------- - -Some of the objects provided by Flask are proxies to other objects. The -reason behind this is that these proxies are shared between threads and -they have to dispatch to the actual object bound to a thread behind the -scenes as necessary. - -Most of the time you don't have to care about that, but there are some -exceptions where it is good to know that this object is an actual proxy: - -- The proxy objects do not fake their inherited types, so if you want to - perform actual instance checks, you have to do that on the instance - that is being proxied (see `_get_current_object` below). -- if the object reference is important (so for example for sending - :ref:`signals`) - -If you need to get access to the underlying object that is proxied, you -can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: - - app = current_app._get_current_object() - my_signal.send(app) diff --git a/docs/config.rst b/docs/config.rst index 90a276cc..aedaae7d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -51,27 +51,36 @@ The following configuration values are used internally by Flask: .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| -=============================== ========================================= -``DEBUG`` enable/disable debug mode -``TESTING`` enable/disable testing mode -``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the - propagation of exceptions. If not set or - explicitly set to `None` this is - implicitly true if either `TESTING` or - `DEBUG` is true. -``SECRET_KEY`` the secret key -``SESSION_COOKIE_NAME`` the name of the session cookie -``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as - :class:`datetime.timedelta` object. -``USE_X_SENDFILE`` enable/disable x-sendfile -``LOGGER_NAME`` the name of the logger -``SERVER_NAME`` the name of the server. Required for - subdomain support (e.g.: ``'localhost'``) -``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will - reject incoming requests with a - content length greater than this by - returning a 413 status code. -=============================== ========================================= +================================= ========================================= +``DEBUG`` enable/disable debug mode +``TESTING`` enable/disable testing mode +``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the + propagation of exceptions. If not set or + explicitly set to `None` this is + implicitly true if either `TESTING` or + `DEBUG` is true. +``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in + debug mode the request context is not + popped on exceptions to enable debuggers + to introspect the data. This can be + disabled by this key. You can also use + this setting to force-enable it for non + debug execution which might be useful to + debug production applications (but also + very risky). +``SECRET_KEY`` the secret key +``SESSION_COOKIE_NAME`` the name of the session cookie +``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as + :class:`datetime.timedelta` object. +``USE_X_SENDFILE`` enable/disable x-sendfile +``LOGGER_NAME`` the name of the logger +``SERVER_NAME`` the name of the server. Required for + subdomain support (e.g.: ``'localhost'``) +``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will + reject incoming requests with a + content length greater than this by + returning a 413 status code. +================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -102,7 +111,7 @@ The following configuration values are used internally by Flask: ``MAX_CONTENT_LENGTH`` .. versionadded:: 0.7 - ``PROPAGATE_EXCEPTIONS`` + ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` Configuring from Files ---------------------- diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index f32d1da5..2689b4b5 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,6 +17,7 @@ instructions for web development with Flask. errorhandling config signals + reqcontext shell patterns/index deploying/index diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst new file mode 100644 index 00000000..088502eb --- /dev/null +++ b/docs/reqcontext.rst @@ -0,0 +1,230 @@ +.. _request-context: + +The Request Context +=================== + +This document describes the behavior in Flask 0.7 which is mostly in line +with the old behavior but has some small, subtle differences. + +One of the design ideas behind Flask is that there are two different +“states” in which code is executed. The application setup state in which +the application implicitly is on the module level. It starts when the +:class:`Flask` object is instantiated, and it implicitly ends when the +first request comes in. While the application is in this state a few +assumptions are true: + +- the programmer can modify the application object safely. +- no request handling happened so far +- you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +On the contrast, during request handling, a couple of other rules exist: + +- while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. +- any code can get hold of these objects at any time. + +The magic that makes this works is internally referred in Flask as the +“request context”. + +Diving into Context Locals +-------------------------- + +Say you have a utility function that returns the URL the user should be +redirected to. Imagine it would always redirect to the URL's ``next`` +parameter or the HTTP referrer or the index page:: + + from flask import request, url_for + + def redirect_url(): + return request.args.get('next') or \ + request.referrer or \ + url_for('index') + +As you can see, it accesses the request object. If you try to run this +from a plain Python shell, this is the exception you will see: + +>>> redirect_url() +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'NoneType' object has no attribute 'request' + +That makes a lot of sense because we currently do not have a request we +could access. So we have to make a request and bind it to the current +context. The :attr:`~flask.Flask.test_request_context` method can create +us a :class:`~flask.ctx.RequestContext`: + +>>> ctx = app.test_request_context('/?next=http://example.com/') + +This context can be used in two ways. Either with the `with` statement +or by calling the :meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods: + +>>> ctx.push() + +From that point onwards you can work with the request object: + +>>> redirect_url() +u'http://example.com/' + +Until you call `pop`: + +>>> ctx.pop() + +Because the request context is internally maintained as a stack you can +push and pop multiple times. This is very handy to implement things like +internal redirects. + +For more information of how to utilize the request context from the +interactive Python shell, head over to the :ref:`shell` chapter. + +How the Context Works +--------------------- + +If you look into how the Flask WSGI application internally works, you will +find a piece of code that looks very much like this:: + + def wsgi_app(self, environ): + with self.request_context(environ): + try: + response = self.full_dispatch_request() + except Exception, e: + response = self.make_response(self.handle_exception(e)) + return response(environ, start_response) + +The method :meth:`~Flask.request_context` returns a new +:class:`~flask.ctx.RequestContext` object and uses it in combination with +the `with` statement to bind the context. Everything that is called from +the same thread from this point onwards until the end of the `with` +statement will have access to the request globals (:data:`flask.request` +and others). + +The request context internally works like a stack: The topmost level on +the stack is the current active request. +:meth:`~flask.ctx.RequestContext.push` adds the context to the stack on +the very top, :meth:`~flask.ctx.RequestContext.pop` removes it from the +stack again. On popping the application's +:func:`~flask.Flask.teardown_request` functions are also executed. + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +What happens if an error occurs in Flask during request processing? This +particular behavior changed in 0.7 because we wanted to make it easier to +understand what is actually happening. The new behavior is quite simple: + +1. Before each request, :meth:`~flask.Flask.before_request` functions are + executed. If one of these functions return a response, the other + functions are no longer called. In any case however the return value + is treated as a replacement for the view's return value. + +2. If the :meth:`~flask.Flask.before_request` functions did not return a + response, the regular request handling kicks in and the view function + that was matched has the chance to return a response. + +3. The return value of the view is then converted into an actual response + object and handed over to the :meth:`~flask.Flask.after_request` + functions which have the chance to replace it or modify it in place. + +4. At the end of the request the :meth:`~flask.Flask.teardown_request` + functions are executed. This always happens, even in case of an + unhandled exception down the road. + +Now what happens on errors? In production mode if an exception is not +caught, the 500 internal server handler is called. In development mode +however the exception is not further processed and bubbles up to the WSGI +server. That way things like the interactive debugger can provide helpful +debug information. + +An important change in 0.7 is that the internal server error is now no +longer post processed by the after request callbacks and after request +callbacks are no longer guaranteed to be executed. This way the internal +dispatching code looks cleaner and is easier to customize and understand. + +The new teardown functions are supposed to be used as a replacement for +things that absolutely need to happen at the end of request. + +Teardown Callbacks +------------------ + +The teardown callbacks are special callbacks in that they are executed at +at different point. Strictly speaking they are independent of the actual +request handling as they are bound to the lifecycle of the +:class:`~flask.ctx.RequestContext` object. When the request context is +popped, the :meth:`~flask.Flask.teardown_request` functions are called. + +This is important to know if the life of the request context is prolonged +by using the test client in a with statement of when using the request +context from the command line:: + + with app.test_client() as client: + resp = client.get('/foo') + # the teardown functions are still not called at that point + # even though the response ended and you have the response + # object in your hand + + # only when the code reaches this point the teardown functions + # are called. Alternatively the same thing happens if another + # request was triggered from the test client + +It's easy to see the behavior from the command line: + +>>> app = Flask(__name__) +>>> @app.teardown_request +... def after_request(exception=None): +... print 'after request' +... +>>> ctx = app.test_request_context() +>>> ctx.push() +>>> ctx.pop() +after request + +.. _notes-on-proxies: + +Notes On Proxies +---------------- + +Some of the objects provided by Flask are proxies to other objects. The +reason behind this is that these proxies are shared between threads and +they have to dispatch to the actual object bound to a thread behind the +scenes as necessary. + +Most of the time you don't have to care about that, but there are some +exceptions where it is good to know that this object is an actual proxy: + +- The proxy objects do not fake their inherited types, so if you want to + perform actual instance checks, you have to do that on the instance + that is being proxied (see `_get_current_object` below). +- if the object reference is important (so for example for sending + :ref:`signals`) + +If you need to get access to the underlying object that is proxied, you +can use the :meth:`~werkzeug.local.LocalProxy._get_current_object` method:: + + app = current_app._get_current_object() + my_signal.send(app) + +Context Preservation on Error +----------------------------- + +If an error occurs or not, at the end of the request the request context +is popped and all data associated with it is destroyed. During +development however that can be problematic as you might want to have the +information around for a longer time in case an exception occurred. In +Flask 0.6 and earlier in debug mode, if an exception occurred, the +request context was not popped so that the interactive debugger can still +provide you with important information. + +Starting with Flask 0.7 you have finer control over that behavior by +setting the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. By +default it's linked to the setting of ``DEBUG``. If the application is in +debug mode the context is preserved, in production mode it's not. + +Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` in production mode +as it will cause your application to leak memory on exceptions. However +it can be useful during development to get the same error preserving +behavior as in development mode when attempting to debug an error that +only occurs under production settings. diff --git a/docs/shell.rst b/docs/shell.rst index 470bceca..61b9dc05 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,3 +1,5 @@ +.. _shell: + Working with the Shell ====================== @@ -21,61 +23,37 @@ that these functions are not only there for interactive shell usage, but also for unittesting and other situations that require a faked request context. -Diving into Context Locals +Generally it's recommended that you read the :ref:`request-context` +chapter of the documentation first. + +Creating a Request Context -------------------------- -Say you have a utility function that returns the URL the user should be -redirected to. Imagine it would always redirect to the URL's ``next`` -parameter or the HTTP referrer or the index page:: +The easiest way to create a proper request context from the shell is by +using the :attr:`~flask.Flask.test_request_context` method which creates +us a :class:`~flask.ctx.RequestContext`: - from flask import request, url_for +>>> ctx = app.test_request_context() - def redirect_url(): - return request.args.get('next') or \ - request.referrer or \ - url_for('index') - -As you can see, it accesses the request object. If you try to run this -from a plain Python shell, this is the exception you will see: - ->>> redirect_url() -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'NoneType' object has no attribute 'request' - -That makes a lot of sense because we currently do not have a request we -could access. So we have to make a request and bind it to the current -context. The :attr:`~flask.Flask.test_request_context` method can create -us a request context: - ->>> ctx = app.test_request_context('/?next=http://example.com/') - -This context can be used in two ways. Either with the `with` statement -(which unfortunately is not very handy for shell sessions). The -alternative way is to call the `push` and `pop` methods: +Normally you would use the `with` statement to make this request object +active, but in the shell it's easier to use the +:meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods by hand: >>> ctx.push() -From that point onwards you can work with the request object: - ->>> redirect_url() -u'http://example.com/' - -Until you call `pop`: +From that point onwards you can work with the request object until you +call `pop`: >>> ctx.pop() ->>> redirect_url() -Traceback (most recent call last): - File "", line 1, in -AttributeError: 'NoneType' object has no attribute 'request' - Firing Before/After Request --------------------------- By just creating a request context, you still don't have run the code that -is normally run before a request. This probably results in your database -being unavailable, the current user not being stored on the +is normally run before a request. This might result in your database +being unavailable if you are connecting to the database in a +before-request callback or the current user not being stored on the :data:`~flask.g` object etc. This however can easily be done yourself. Just call @@ -96,6 +74,11 @@ a response object: >>> ctx.pop() +The functions registered as :meth:`~flask.Flask.teardown_request` are +automatically called when the context is popped. So this is the perfect +place to automatically tear down resources that were needed by the request +context (such as database connections). + Further Improving the Shell Experience -------------------------------------- diff --git a/docs/signals.rst b/docs/signals.rst index ed5ecd51..a5821603 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -236,4 +236,20 @@ The following signals exist in Flask: from flask import got_request_exception got_request_exception.connect(log_exception, app) +.. data:: flask.request_tearing_down + :noindex: + + This signal is sent when the request is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender): + session.close() + + from flask import request_tearing_down + request_tearing_down.connect(close_db_connection, app) + .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/__init__.py b/flask/__init__.py index 1274e766..f9fcc09f 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -28,7 +28,7 @@ from .session import Session # the signals from .signals import signals_available, template_rendered, request_started, \ - request_finished, got_request_exception + request_finished, got_request_exception, request_tearing_down # only import json if it's available if json_available: diff --git a/flask/app.py b/flask/app.py index 1e068e41..99d7a266 100644 --- a/flask/app.py +++ b/flask/app.py @@ -27,13 +27,14 @@ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ _tojson_filter, _endpoint_from_view_func from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import _RequestContext +from .ctx import RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState from .templating import _DispatchingJinjaLoader, \ _default_template_ctx_processor -from .signals import request_started, request_finished, got_request_exception +from .signals import request_started, request_finished, got_request_exception, \ + request_tearing_down # a lock used for logger initialization _logger_lock = Lock() @@ -126,6 +127,9 @@ class Flask(_PackageBoundObject): #: For example this might activate unittest helpers that have an #: additional runtime cost which should not be enabled by default. #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: #: This attribute can also be configured from the config with the #: `TESTING` configuration key. Defaults to `False`. testing = ConfigAttribute('TESTING') @@ -191,6 +195,7 @@ class Flask(_PackageBoundObject): 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, + 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), @@ -334,6 +339,19 @@ class Flask(_PackageBoundObject): return rv return self.testing or self.debug + @property + def preserve_context_on_exception(self): + """Returns the value of the `PRESERVE_CONTEXT_ON_EXCEPTION` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PRESERVE_CONTEXT_ON_EXCEPTION'] + if rv is not None: + return rv + return self.debug + @property def logger(self): """A :class:`logging.Logger` object for this application. The @@ -713,16 +731,38 @@ class Flask(_PackageBoundObject): return f def after_request(self, f): - """Register a function to be run after each request. Your function + """Register a function to be run after each request. Your function must take one parameter, a :attr:`response_class` object and return a new response object or the same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception ocurred. """ self.after_request_funcs.setdefault(None, []).append(f) return f def teardown_request(self, f): """Register a function to be run at the end of each request, - regardless of whether there was an exception or not. + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necesary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log ocurring errors. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f @@ -770,21 +810,39 @@ class Flask(_PackageBoundObject): return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. """ req = _request_ctx_stack.top.request + if req.routing_exception is not None: + raise req.routing_exception + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) + + def full_dispatch_request(self): + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + """ try: - if req.routing_exception is not None: - raise req.routing_exception - rule = req.url_rule - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if getattr(rule, 'provide_automatic_options', False) \ - and req.method == 'OPTIONS': - return self.make_default_options_response() - # otherwise dispatch to the handler for that endpoint - return self.view_functions[rule.endpoint](**req.view_args) + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() except HTTPException, e: - return self.handle_http_exception(e) + rv = self.handle_http_exception(e) + response = self.make_response(rv) + response = self.process_response(response) + request_finished.send(self, response=response) + return response def make_default_options_response(self): """This method is called to create the default `OPTIONS` response. @@ -894,7 +952,10 @@ class Flask(_PackageBoundObject): def do_teardown_request(self): """Called after the actual request dispatching and will - call every as :meth:`teardown_request` decorated function. + call every as :meth:`teardown_request` decorated function. This is + not actually called by the :class:`Flask` object itself but is always + triggered when the request context is popped. That way we have a + tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) mod = request.module @@ -905,12 +966,13 @@ class Flask(_PackageBoundObject): rv = func(exc) if rv is not None: return rv + request_tearing_down.send(self) def request_context(self, environ): - """Creates a request context from the given environment and binds - it to the current context. This must be used in combination with - the `with` statement because the request is only bound to the - current context for the duration of the `with` block. + """Creates a :class:`~flask.ctx.RequestContext` from the given + environment and binds it to the current context. This must be used in + combination with the `with` statement because the request is only bound + to the current context for the duration of the `with` block. Example usage:: @@ -934,7 +996,7 @@ class Flask(_PackageBoundObject): :param environ: a WSGI environment """ - return _RequestContext(self, environ) + return RequestContext(self, environ) def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see @@ -969,16 +1031,11 @@ class Flask(_PackageBoundObject): Then you still have the original application object around and can continue to call methods on it. - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. - .. versionchanged:: 0.7 - The :meth:`teardown_request` functions get called at the very end of - processing the request. If an exception was thrown, it gets passed to - each teardown_request function. + The behavior of the before and after request callbacks was changed + under error conditions and a new callback was added that will + always execute at the end of the request, independent on if an + error ocurred or not. See :ref:`callbacks-and-errors`. :param environ: a WSGI environment :param start_response: a callable accepting a status code, @@ -987,20 +1044,9 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) + response = self.full_dispatch_request() except Exception, e: response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - finally: - self.do_teardown_request() - request_finished.send(self, response=response) return response(environ, start_response) def __call__(self, environ, start_response): diff --git a/flask/ctx.py b/flask/ctx.py index b63f09f1..bc4877cd 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -51,11 +51,34 @@ def has_request_context(): return _request_ctx_stack.top is not None -class _RequestContext(object): +class RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the `_request_ctx_stack` and removed at the end of it. It will create the URL adapter and request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of `DEBUG` mode. By setting + ``'flask._preserve_context'`` to `True` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. """ def __init__(self, app, environ): @@ -74,7 +97,7 @@ class _RequestContext(object): self.request.routing_exception = e def push(self): - """Binds the request context.""" + """Binds the request context to the current context.""" _request_ctx_stack.push(self) # Open the session at the moment that the request context is @@ -85,7 +108,11 @@ class _RequestContext(object): self.session = _NullSession() def pop(self): - """Pops the request context.""" + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + """ + self.app.do_teardown_request() _request_ctx_stack.pop() def __enter__(self): @@ -99,5 +126,5 @@ class _RequestContext(object): # the context can be force kept alive for the test client. # See flask.testing for how this works. if not self.request.environ.get('flask._preserve_context') and \ - (tb is None or not self.app.debug): + (tb is None or not self.app.preserve_context_on_exception): self.pop() diff --git a/flask/signals.py b/flask/signals.py index 22447c7c..4eedf68f 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -47,4 +47,5 @@ _signals = Namespace() template_rendered = _signals.signal('template-rendered') request_started = _signals.signal('request-started') request_finished = _signals.signal('request-finished') +request_tearing_down = _signals.signal('request-tearing-down') got_request_exception = _signals.signal('got-request-exception') diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b723f217..ba3dbcdf 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -784,8 +784,11 @@ class BasicFunctionalityTestCase(unittest.TestCase): def test_max_content_length(self): app = flask.Flask(__name__) - app.debug = True app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + assert False @app.route('/accept', methods=['POST']) def accept_file(): flask.request.form['myfile'] From ba6bf23e0d08ca0b9163b4e5c37b3d2fd274b1b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:12:20 +0200 Subject: [PATCH 0592/3747] Updated tests --- tests/flask_tests.py | 46 +++++++++++++------------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ba3dbcdf..387a0589 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -398,37 +398,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'after' in evts assert rv == 'request|after' - def test_after_request_errors(self): - app = flask.Flask(__name__) - called = [] - @app.after_request - def after_request(response): - called.append(True) - return response - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 1 - - def test_after_request_handler_error(self): - called = [] - app = flask.Flask(__name__) - @app.after_request - def after_request(response): - called.append(True) - 1/0 - return response - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 1 - def test_teardown_request_handler(self): called = [] app = flask.Flask(__name__) @@ -460,7 +429,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Response' in rv.data assert len(called) == 1 - def test_teardown_request_handler_error(self): called = [] app = flask.Flask(__name__) @@ -494,7 +462,6 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'Internal Server Error' in rv.data assert len(called) == 2 - def test_before_after_request_order(self): called = [] app = flask.Flask(__name__) @@ -547,6 +514,19 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + assert buffer == [] + ctx.pop() + assert buffer == [None] + def test_response_creation(self): app = flask.Flask(__name__) @app.route('/unicode') From a9fc040c39109f86f3ea98440215b4a54f291f53 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:21:41 +0200 Subject: [PATCH 0593/3747] Updated documentation to use teardown request where appropriate --- docs/extensiondev.rst | 12 +++++------- docs/patterns/sqlalchemy.rst | 10 ++++------ docs/patterns/sqlite3.rst | 32 ++++++++++++++++++++++++++++---- docs/tutorial/dbcon.rst | 31 ++++++++----------------------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index a5cc2aa4..2c79c6ab 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -182,7 +182,7 @@ Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: def __init__(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.after_request(self.after_request) + self.app.teardown_request(self.teardown_request) self.app.before_request(self.before_request) def connect(self): @@ -192,10 +192,9 @@ Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: ctx = _request_ctx_stack.top ctx.sqlite3_db = self.connect() - def after_request(self, response): + def teardown_request(self, exception): ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - return response def get_db(self): ctx = _request_ctx_stack.top @@ -211,7 +210,7 @@ So here's what these lines of code do: 2. We create a class for our extension that requires a supplied `app` object, sets a configuration for the database if it's not there (:meth:`dict.setdefault`), and attaches `before_request` and - `after_request` handlers. + `teardown_request` handlers. 3. Next, we define a `connect` function that opens a database connection. 4. Then we set up the request handlers we bound to the app above. Note here that we're attaching our database connection to the top request context via @@ -264,7 +263,7 @@ Our extension could add an `init_app` function as follows:: def init_app(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.after_request(self.after_request) + self.app.teardown_request(self.teardown_request) self.app.before_request(self.before_request) def connect(self): @@ -274,10 +273,9 @@ Our extension could add an `init_app` function as follows:: ctx = _request_ctx_stack.top ctx.sqlite3_db = self.connect() - def after_request(self, response): + def teardown_request(self, exception): ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - return response def get_db(self): ctx = _request_ctx_stack.top diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 24e9f013..5a33d1f6 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -65,10 +65,9 @@ automatically remove database sessions at the end of the request for you:: from yourapplication.database import db_session - @app.after_request - def shutdown_session(response): + @app.teardown_request + def shutdown_session(exception=None): db_session.remove() - return response Here is an example model (put this into `models.py`, e.g.):: @@ -140,10 +139,9 @@ each request. Put this into your application module:: from yourapplication.database import db_session - @app.after_request - def shutdown_session(response): + @app.teardown_request + def shutdown_session(exception=None): db_session.remove() - return response Here is an example table and model (put this into `models.py`):: diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 68833234..d0ec5a27 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -5,7 +5,7 @@ Using SQLite 3 with Flask In Flask you can implement the opening of database connections at the beginning of the request and closing at the end with the -:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.teardown_request` decorators in combination with the special :class:`~flask.g` object. So here is a simple example of how you can use SQLite 3 with Flask:: @@ -22,10 +22,34 @@ So here is a simple example of how you can use SQLite 3 with Flask:: def before_request(): g.db = connect_db() - @app.after_request - def after_request(response): + @app.teardown_request + def teardown_request(exception): g.db.close() - return response + +Connect on Demand +----------------- + +The downside of this approach is that this will only work if Flask +executed the before-request handlers for you. If you are attempting to +use the database from a script or the interactive Python shell you would +have to do something like this:: + + with app.test_request_context() + app.preprocess_request() + # now you can use the g.db object + +In order to trigger the execution of the connection code. You won't be +able to drop the dependency on the request context this way, but you could +make it so that the application connects when necessary:: + + def get_connection(): + db = getattr(g, '_db', None) + if db is None: + db = g._db = connect_db() + return db + +Downside here is that you have to use ``db = get_connection()`` instead of +just being able to use ``g.db`` directly. .. _easy-querying: diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index b2a626f9..1d9d41f9 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -9,22 +9,8 @@ connection in all our functions so it makes sense to initialize them before each request and shut them down afterwards. Flask allows us to do that with the :meth:`~flask.Flask.before_request`, -:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request` -decorators. In debug mode, if an error is raised, -:meth:`~flask.Flask.after_request` won't be run, and you'll have access to the -db connection in the interactive debugger:: - - @app.before_request - def before_request(): - g.db = connect_db() - - @app.after_request - def after_request(response): - g.db.close() - return response - -If you want to guarantee that the connection is always closed in debug mode, you -can close it in a function decorated with :meth:`~flask.Flask.teardown_request`:: +:meth:`~flask.Flask.teardown_request` and :meth:`~flask.Flask.teardown_request` +decorators:: @app.before_request def before_request(): @@ -36,18 +22,17 @@ can close it in a function decorated with :meth:`~flask.Flask.teardown_request`: Functions marked with :meth:`~flask.Flask.before_request` are called before a request and passed no arguments. Functions marked with -:meth:`~flask.Flask.after_request` are called after a request and +:meth:`~flask.Flask.teardown_request` are called after a request and passed the response that will be sent to the client. They have to return -that response object or a different one. In this case we just return it -unchanged. - -Functions marked with :meth:`~flask.Flask.teardown_request` get called after the +that response object or a different one. They are however not guaranteed +to be executed if an exception is raised, this is where functions marked with +:meth:`~flask.Flask.teardown_request` come in. They get called after the response has been constructed. They are not allowed to modify the request, and their return values are ignored. If an exception occurred while the request was -being processed, it is passed to each function; otherwise, None is passed in. +being processed, it is passed to each function; otherwise, `None` is passed in. We store our current database connection on the special :data:`~flask.g` -object that flask provides for us. This object stores information for one +object that Flask provides for us. This object stores information for one request only and is available from within each function. Never store such things on other objects because this would not work with threaded environments. That special :data:`~flask.g` object does some magic behind From 115d31ddbfac0773794f115eada88a75d651a0f2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:29:03 +0200 Subject: [PATCH 0594/3747] More documentation updates --- CHANGES | 5 ++++- docs/upgrading.rst | 47 ++++++++++++++++++++++++++++++++++++++++++++++ flask/app.py | 2 ++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e10744ca..d8d12733 100644 --- a/CHANGES +++ b/CHANGES @@ -38,13 +38,16 @@ Release date to be announced, codename to be selected - Added `create_jinja_loader` to override the loader creation process. - Implemented a silent flag for `config.from_pyfile`. - Added `teardown_request` decorator, for functions that should run at the end - of a request regardless of whether an exception occurred. + of a request regardless of whether an exception occurred. Also the behavior + for `after_request` was changed. It's now no longer executed when an exception + is raised. See :ref:`upgrading-to-new-teardown-handling` - Implemented :func:`flask.has_request_context` - Added :func:`safe_join` - The automatic JSON request data unpacking now looks at the charset mimetype parameter. - Don't modify the session on :func:`flask.get_flashed_messages` if there are no messages in the session. +- `before_request` handlers are now able to abort requests with errors. Version 0.6.1 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 17523290..7d6e0f0a 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -22,6 +22,11 @@ installation, make sure to pass it the ``-U`` parameter:: Version 0.7 ----------- +The following backwards incompatible changes exist from 0.6 to 0.7 + +Bug in Request Locals +````````````````````` + Due to a bug in earlier implementations the request local proxies now raise a :exc:`RuntimeError` instead of an :exc:`AttributeError` when they are unbound. If you caught these exceptions with :exc:`AttributeError` @@ -44,6 +49,48 @@ New code:: return send_file(my_file_object, add_etags=False) +.. _upgrading-to-new-teardown-handling: + +Upgrading to new Teardown Handling +`````````````````````````````````` + +We streamlined the behavior of the callbacks for request handling. For +things that modify the response the :meth:`~flask.Flask.after_request` +decorators continue to work as expected, but for things that absolutely +must happen at the end of request we introduced the new +:meth:`~flask.Flask.teardown_request` decorator. Unfortunately that +change also made after-request work differently under error conditions. +It's not consistently skipped if exceptions happen whereas previously it +might have been called twice to ensure it is executed at the end of the +request. + +If you have database connection code that looks like this:: + + @app.after_request + def after_request(response): + g.db.close() + return response + +You are now encouraged to use this instead:: + + @app.teardown_request + def after_request(exception): + g.db.close() + +On the upside this change greatly improves the internal code flow and +makes it easier to customize the dispatching and error handling. This +makes it now a lot easier to write unit tests as you can prevent closing +down of database connections for a while. You can take advantage of the +fact that the teardown callbacks are called when the response context is +removed from the stack so a test can query the database after request +handling:: + + with app.test_client() as client: + resp = client.get('/') + # g.db is still bound if there is such a thing + + # and here it's gone + Version 0.6 ----------- diff --git a/flask/app.py b/flask/app.py index 99d7a266..c1bcc340 100644 --- a/flask/app.py +++ b/flask/app.py @@ -831,6 +831,8 @@ class Flask(_PackageBoundObject): """Dispatches the request and on top of that performs request pre and postprocessing as well as HTTP exception catching and error handling. + + .. versionadded:: 0.7 """ try: request_started.send(self) From b51ecd7f212588b3d7363db90a4ab8318ec2fa80 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 27 May 2011 20:29:47 +0200 Subject: [PATCH 0595/3747] Updated examples --- examples/flaskr/flaskr.py | 5 ++--- examples/minitwit/minitwit.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 69953555..361c1aee 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -47,11 +47,10 @@ def before_request(): g.db = connect_db() -@app.after_request -def after_request(response): +@app.teardown_request +def teardown_request(exception): """Closes the database again at the end of the request.""" g.db.close() - return response @app.route('/') diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 7726e9f4..f7c700d3 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -82,11 +82,10 @@ def before_request(): [session['user_id']], one=True) -@app.after_request -def after_request(response): +@app.teardown_request +def teardown_request(exception): """Closes the database again at the end of the request.""" g.db.close() - return response @app.route('/') From 086ecdb91829d43a9b948c6d97524d8de18e2fa4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 28 May 2011 15:11:48 +0200 Subject: [PATCH 0596/3747] Better reraising of exceptions --- docs/config.rst | 3 ++- docs/extensiondev.rst | 23 +++++++++++++++++++++++ flask/app.py | 13 ++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index aedaae7d..dc761657 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -240,6 +240,7 @@ your configuration files. However here a list of good recommendations: and exports the development configuration for you. - Use a tool like `fabric`_ in production to push code and configurations separately to the production server(s). For some - details about how to do that, head over to the :ref:`deploy` pattern. + details about how to do that, head over to the + :ref:`fabric-deployment` pattern. .. _fabric: http://fabfile.org/ diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 2c79c6ab..6b407b34 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -290,6 +290,29 @@ and bind their app to the extension in another file:: manager.init_app(app) +End-Of-Request Behavior +----------------------- + +Due to the change in Flask 0.7 regarding functions that are run at the end +of the request your extension will have to be extra careful there if it +wants to continue to support older versions of Flask. The following +pattern is a good way to support both:: + + def close_connection(response): + ctx = _request_ctx_stack.top + ctx.sqlite3_db.close() + return response + + if hasattr(app, 'teardown_request'): + app.teardown_request(close_connection) + else: + app.after_request(close_connection) + +Strictly speaking the above code is wrong, because teardown functions are +passed the exception and typically don't return anything. However because +the return value is discarded this will just work assuming that the code +in between does not touch the passed parameter. + Learn from Others ----------------- diff --git a/flask/app.py b/flask/app.py index c1bcc340..36f91128 100644 --- a/flask/app.py +++ b/flask/app.py @@ -793,10 +793,21 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ + exc_type, exc_value, tb = sys.exc_info() + got_request_exception.send(self, exception=e) handler = self.error_handlers.get(500) + if self.propagate_exceptions: - raise + # if we want to repropagate the exception, we can attempt to + # raise it with the whole traceback in case we can do that + # (the function was actually called from the except part) + # otherwise, we just raise the error again + if exc_value is e: + raise exc_type, exc_value, tb + else: + raise e + self.logger.exception('Exception on %s [%s]' % ( request.path, request.method From 7a08331ac062ee1e2ae3c7547f77c2d342ddb3df Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 15:54:58 +0200 Subject: [PATCH 0597/3747] Latest iteration of the blueprint code. Far from being done --- flask/app.py | 83 ++++++++-------- flask/blueprints.py | 172 +++++++++++++++++++++++++++++++++ flask/ctx.py | 11 +++ flask/helpers.py | 62 ++++++++---- flask/module.py | 221 ++++--------------------------------------- flask/templating.py | 5 +- flask/wrappers.py | 23 +++-- tests/flask_tests.py | 1 + 8 files changed, 304 insertions(+), 274 deletions(-) create mode 100644 flask/blueprints.py diff --git a/flask/app.py b/flask/app.py index 2ed8a65f..2a83a020 100644 --- a/flask/app.py +++ b/flask/app.py @@ -28,7 +28,7 @@ from .config import ConfigAttribute, Config from .ctx import RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession -from .module import _ModuleSetupState +from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ @@ -103,14 +103,6 @@ class Flask(_PackageBoundObject): #: :class:`~flask.Response` for more information. response_class = Response - #: Path for the static files. If you don't want to use static files - #: you can set this value to `None` in which case no URL rule is added - #: and the development server will no longer serve any static files. - #: - #: This is the default used for application and modules unless a - #: different value is passed to the constructor. - static_path = '/static' - #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload @@ -213,10 +205,19 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 test_client_class = None - def __init__(self, import_name, static_path=None): + def __init__(self, import_name, static_path=None, static_url_path=None, + static_folder='static'): _PackageBoundObject.__init__(self, import_name) if static_path is not None: - self.static_path = static_path + from warnings import warn + warn(DeprecationWarning('static_path is now called ' + 'static_url_path'), stacklevel=2) + static_url_path = static_path + + if static_url_path is not None: + self.static_url_path = static_url_path + if static_folder is not None: + self.static_folder = static_folder #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods @@ -242,14 +243,14 @@ class Flask(_PackageBoundObject): #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of - #: the module this function is active for, `None` for all requests. + #: the blueprint this function is active for, `None` for all requests. #: This can for example be used to open database connections or #: getting hold of the currently logged in user. To register a #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the module + #: each request. The key of the dictionary is the name of the blueprint #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the #: currently logged in user. To register a function here, use the @@ -258,7 +259,7 @@ class Flask(_PackageBoundObject): #: A dictionary with lists of functions that are called after #: each request, even if an exception has occurred. The key of the - #: dictionary is the name of the module this function is active for, + #: dictionary is the name of the blueprint this function is active for, #: `None` for all requests. These functions are not allowed to modify #: the request, and their return values are ignored. If an exception #: occurred while processing the request, it gets passed to each @@ -270,7 +271,7 @@ class Flask(_PackageBoundObject): #: A dictionary with list of functions that are called without argument #: to populate the template context. The key of the dictionary is the - #: name of the module this function is active for, `None` for all + #: name of the blueprint this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is #: updated with. To register a function here, use the #: :meth:`context_processor` decorator. @@ -278,11 +279,6 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } - #: all the loaded modules in a dictionary by name. - #: - #: .. versionadded:: 0.5 - self.modules = {} - #: all the attached blueprints in a directory by name. Blueprints #: can be attached multiple times so this dictionary does not tell #: you how often they got attached. @@ -328,9 +324,10 @@ class Flask(_PackageBoundObject): # while the server is running (usually happens during development) # but also because google appengine stores static files somewhere # else when mapped with the .yml file. - self.add_url_rule(self.static_path + '/', - endpoint='static', - view_func=self.send_static_file) + if self.has_static_folder: + self.add_url_rule(self.static_url_path + '/', + endpoint='static', + view_func=self.send_static_file) @property def propagate_exceptions(self): @@ -456,9 +453,9 @@ class Flask(_PackageBoundObject): to add extra variables. """ funcs = self.template_context_processors[None] - mod = _request_ctx_stack.top.request.module - if mod is not None and mod in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[mod]) + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() for func in funcs: context.update(func()) @@ -565,6 +562,8 @@ class Flask(_PackageBoundObject): The module system was deprecated in favor for the blueprint system. """ + assert blueprint_is_module(module), 'register_module requires ' \ + 'actual module objects. Please upgrade to blueprints though.' if not self.enable_modules: raise RuntimeError('Module support was disabled but code ' 'attempted to register a module named %r' % module) @@ -577,12 +576,7 @@ class Flask(_PackageBoundObject): 'of that extension instead. (Registered %r)' % module), stacklevel=2) - options.setdefault('url_prefix', module.url_prefix) - options.setdefault('subdomain', module.subdomain) - self.view_functions.update(module.view_functions) - state = _ModuleSetupState(self, **options) - for func in module._register_events: - func(state) + self.register_blueprint(module, **options) def register_blueprint(self, blueprint, **options): """Registers a blueprint on the application. @@ -987,9 +981,9 @@ class Flask(_PackageBoundObject): request handling is stopped. """ funcs = self.before_request_funcs.get(None, ()) - mod = request.module - if mod and mod in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[mod]) + bp = request.blueprint + if bp is not None and bp in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[bp]) for func in funcs: rv = func() if rv is not None: @@ -1009,12 +1003,12 @@ class Flask(_PackageBoundObject): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - mod = ctx.request.module + bp = ctx.request.blueprint if not isinstance(ctx.session, _NullSession): self.save_session(ctx.session, response) funcs = () - if mod and mod in self.after_request_funcs: - funcs = reversed(self.after_request_funcs[mod]) + if bp is not None and bp in self.after_request_funcs: + funcs = reversed(self.after_request_funcs[bp]) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: @@ -1029,9 +1023,9 @@ class Flask(_PackageBoundObject): tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) - mod = request.module - if mod and mod in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[mod])) + bp = request.blueprint + if bp is not None and bp in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) exc = sys.exc_info()[1] for func in funcs: rv = func(exc) @@ -1120,6 +1114,13 @@ class Flask(_PackageBoundObject): response = self.make_response(self.handle_exception(e)) return response(environ, start_response) + @property + def modules(self): + from warnings import warn + warn(DeprecationWarning('Flask.modules is deprecated, use ' + 'Flask.blueprints instead'), stacklevel=2) + return self.blueprints + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) diff --git a/flask/blueprints.py b/flask/blueprints.py new file mode 100644 index 00000000..96a97d30 --- /dev/null +++ b/flask/blueprints.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +""" + flask.blueprints + ~~~~~~~~~~~~~~~~ + + Blueprints are the recommended way to implement larger or more + pluggable applications in Flask 0.7 and later. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os + +from .helpers import _PackageBoundObject, _endpoint_from_view_func + + +class BlueprintSetupState(object): + """Temporary holder object for registering a blueprint with the + application. + """ + + def __init__(self, blueprint, app, options): + self.app = app + self.blueprint = blueprint + self.options = options + + subdomain = self.options.get('subdomain') + if subdomain is None: + subdomain = self.blueprint.subdomain + self.subdomain = subdomain + + url_prefix = self.options.get('url_prefix') + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + self.url_prefix = url_prefix + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + if self.url_prefix: + rule = self.url_prefix + rule + options.setdefault('subdomain', self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), + view_func, **options) + + +class Blueprint(_PackageBoundObject): + """Represents a blueprint. + + .. versionadded:: 0.7 + """ + + def __init__(self, name, import_name, static_folder=None, + static_url_path=None, url_prefix=None, + subdomain=None): + _PackageBoundObject.__init__(self, import_name) + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.static_folder = static_folder + self.static_url_path = static_url_path + self.deferred_functions = [] + + def _record(self, func): + self.deferred_functions.append(func) + + def make_setup_state(self, app, options): + return BlueprintSetupState(self, app, options) + + def register(self, app, options): + """Called by :meth:`Flask.register_blueprint` to register a blueprint + on the application. This can be overridden to customize the register + behavior. Keyword arguments from + :func:`~flask.Flask.register_blueprint` are directly forwarded to this + method in the `options` dictionary. + """ + state = self.make_setup_state(app, options) + if self.has_static_folder: + state.add_url_rule(self.static_url_path + '/', + view_func=self.send_static_file, + endpoint='static') + + for deferred in self.deferred_functions: + deferred(state) + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a module. The endpoint for the + :func:`url_for` function is prefixed with the name of the module. + """ + def decorator(f): + self.add_url_rule(rule, f.__name__, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for + the :func:`url_for` function is prefixed with the name of the module. + """ + def register_rule(state): + state.add_url_rule(rule, endpoint, view_func, **options) + self._record(register_rule) + + def endpoint(self, endpoint): + """Like :meth:`Flask.endpoint` but for a module. This does not + prefix the endpoint with the module name, this has to be done + explicitly by the user of this method. + """ + def decorator(f): + def register_endpoint(state): + state.app.view_functions[endpoint] = f + self._record(register_endpoint) + return f + return decorator + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a module. This function + is only executed before each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a module. This function + is only executed after each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a module. Such a function + is executed after each request, even if outside of the module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. This + function is only executed for requests handled by a module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. Such a + function is executed each request, even if outside of the module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a module. This + handler is used for all requests, even if outside of the module. + """ + def decorator(f): + self._record(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator diff --git a/flask/ctx.py b/flask/ctx.py index bc4877cd..1e700a84 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -13,6 +13,7 @@ from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack from .session import _NullSession +from .module import blueprint_is_module class _RequestGlobals(object): @@ -96,6 +97,16 @@ class RequestContext(object): except HTTPException, e: self.request.routing_exception = e + # Support for deprecated functionality. This is doing away with + # Flask 1.0 + blueprint = self.request.blueprint + if blueprint is not None: + # better safe than sorry, we don't want to break code that + # already worked + bp = app.blueprints.get(blueprint) + if bp is not None and blueprint_is_module(bp): + self.request._is_old_module = True + def push(self): """Binds the request context to the current context.""" _request_ctx_stack.push(self) diff --git a/flask/helpers.py b/flask/helpers.py index 45ecf349..a2afa0f6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -156,19 +156,6 @@ def make_response(*args): def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. - The endpoint is relative to the active module if modules are in use. - - Here are some examples: - - ==================== ======================= ============================= - Active Module Target Endpoint Target Function - ==================== ======================= ============================= - `None` ``'index'`` `index` of the application - `None` ``'.index'`` `index` of the application - ``'admin'`` ``'index'`` `index` of the `admin` module - any ``'.index'`` `index` of the application - any ``'admin.index'`` `index` of the `admin` module - ==================== ======================= ============================= Variable arguments that are unknown to the target endpoint are appended to the generated URL as query arguments. If the value of a query argument @@ -181,12 +168,17 @@ def url_for(endpoint, **values): :param _external: if set to `True`, an absolute URL is generated. """ ctx = _request_ctx_stack.top - if '.' not in endpoint: - mod = ctx.request.module - if mod is not None: - endpoint = mod + '.' + endpoint - elif endpoint.startswith('.'): - endpoint = endpoint[1:] + if not ctx.request._is_old_module: + if endpoint[:1] == '.': + endpoint = request.blueprint + endpoint + else: + # TODO: get rid of this deprecated functionality in 1.0 + if '.' not in endpoint: + mod = ctx.request.blueprint + if mod is not None: + endpoint = mod + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] external = values.pop('_external', False) return ctx.url_adapter.build(endpoint, values, force_external=external) @@ -489,6 +481,8 @@ class locked_cached_property(object): class _PackageBoundObject(object): + template_folder = 'templates' + def __init__(self, import_name): #: The name of the package or module. Do not change this once #: it was set by the constructor. @@ -497,6 +491,28 @@ class _PackageBoundObject(object): #: Where is the app root located? self.root_path = _get_package_path(self.import_name) + self._static_folder = None + self._static_url_path = None + + def _get_static_folder(self): + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + def _set_static_folder(self, value): + self._static_folder = value + static_folder = property(_get_static_folder, _set_static_folder) + del _get_static_folder, _set_static_folder + + def _get_static_url_path(self): + if self._static_url_path is None: + if self.static_folder is None: + return None + return '/' + os.path.basename(self.static_folder) + return self._static_url_path + def _set_static_url_path(self, value): + self._static_url_path = value + static_url_path = property(_get_static_url_path, _set_static_url_path) + del _get_static_url_path, _set_static_url_path + @property def has_static_folder(self): """This is `True` if the package bound object's container has a @@ -504,7 +520,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - return os.path.isdir(os.path.join(self.root_path, 'static')) + return self.static_folder is not None @locked_cached_property def jinja_loader(self): @@ -512,7 +528,9 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - return FileSystemLoader(os.path.join(self.root_path, 'templates')) + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, + self.template_folder)) def send_static_file(self, filename): """Function used internally to send static files from the static @@ -520,6 +538,8 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ + if not self.has_static_folder: + raise RuntimeError('No static folder for this object') return send_from_directory(os.path.join(self.root_path, 'static'), filename) diff --git a/flask/module.py b/flask/module.py index 5c914991..2e9b9db4 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,109 +9,25 @@ :license: BSD, see LICENSE for more details. """ +import os + from .helpers import _PackageBoundObject, _endpoint_from_view_func +from .blueprints import Blueprint -def _register_module(module, static_path): - """Internal helper function that returns a function for recording - that registers the `send_static_file` function for the module on - the application if necessary. It also registers the module on - the application. - """ - def _register(state): - state.app.modules[module.name] = module - # do not register the rule if the static folder of the - # module is the same as the one from the application. - if state.app.root_path == module.root_path: - return - path = static_path - if path is None: - path = state.app.static_path - if state.url_prefix: - path = state.url_prefix + path - state.app.add_url_rule(path + '/', - endpoint='%s.static' % module.name, - view_func=module.send_static_file, - subdomain=state.subdomain) - return _register +def blueprint_is_module(bp): + """Used to figure out if something is actually a module""" + return isinstance(bp, Module) -class _ModuleSetupState(object): +class Module(Blueprint): + """Deprecated module support. Until Flask 0.6 modules were a different + name of the concept now available as blueprints in Flask. They are + essentially doing the same but have some bad semantics for templates and + static files that were fixed with blueprints. - def __init__(self, app, url_prefix=None, subdomain=None): - self.app = app - self.url_prefix = url_prefix - self.subdomain = subdomain - - -class Module(_PackageBoundObject): - """Container object that enables pluggable applications. A module can - be used to organize larger applications. They represent blueprints that, - in combination with a :class:`Flask` object are used to create a large - application. - - A module is like an application bound to an `import_name`. Multiple - modules can share the same import names, but in that case a `name` has - to be provided to keep them apart. If different import names are used, - the rightmost part of the import name is used as name. - - Here's an example structure for a larger application:: - - /myapplication - /__init__.py - /views - /__init__.py - /admin.py - /frontend.py - - The `myapplication/__init__.py` can look like this:: - - from flask import Flask - from myapplication.views.admin import admin - from myapplication.views.frontend import frontend - - app = Flask(__name__) - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - - And here's an example view module (`myapplication/views/admin.py`):: - - from flask import Module - - admin = Module(__name__) - - @admin.route('/') - def index(): - pass - - @admin.route('/login') - def login(): - pass - - For a gentle introduction into modules, checkout the - :ref:`working-with-modules` section. - - .. versionadded:: 0.5 - The `static_path` parameter was added and it's now possible for - modules to refer to their own templates and static files. See - :ref:`modules-and-resources` for more information. - - .. versionadded:: 0.6 - The `subdomain` parameter was added. - - :param import_name: the name of the Python package or module - implementing this :class:`Module`. - :param name: the internal short name for the module. Unless specified - the rightmost part of the import name - :param url_prefix: an optional string that is used to prefix all the - URL rules of this module. This can also be specified - when registering the module with the application. - :param subdomain: used to set the subdomain setting for URL rules that - do not have a subdomain setting set. - :param static_path: can be used to specify a different path for the - static files on the web. Defaults to ``/static``. - This does not affect the folder the files are served - *from*. + .. versionchanged:: 0.7 + Modules were deprecated in favor for blueprints. """ def __init__(self, import_name, name=None, url_prefix=None, @@ -120,111 +36,8 @@ class Module(_PackageBoundObject): assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' name = import_name.rsplit('.', 1)[1] - _PackageBoundObject.__init__(self, import_name) - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.view_functions = {} - self._register_events = [_register_module(self, static_path)] + Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, + subdomain=subdomain) - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a module. The endpoint for the - :func:`url_for` function is prefixed with the name of the module. - """ - def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) - return f - return decorator - - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): - """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for - the :func:`url_for` function is prefixed with the name of the module. - - .. versionchanged:: 0.6 - The `endpoint` argument is now optional and will default to the - function name to consistent with the function of the same name - on the application object. - """ - def register_rule(state): - the_rule = rule - if state.url_prefix: - the_rule = state.url_prefix + rule - options.setdefault('subdomain', state.subdomain) - the_endpoint = endpoint - if the_endpoint is None: - the_endpoint = _endpoint_from_view_func(view_func) - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, - the_endpoint), - view_func, **options) - self._record(register_rule) - - def endpoint(self, endpoint): - """Like :meth:`Flask.endpoint` but for a module.""" - def decorator(f): - self.view_functions[endpoint] = f - return f - return decorator - - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a module. This function - is only executed before each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def before_app_request(self, f): - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(None, []).append(f)) - return f - - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a module. This function - is only executed after each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def after_app_request(self, f): - """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request, even if outside of the module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(None, []).append(f)) - return f - - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. This - function is only executed for requests handled by a module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(self.name, []).append(f)) - return f - - def app_context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. Such a - function is executed each request, even if outside of the module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(None, []).append(f)) - return f - - def app_errorhandler(self, code): - """Like :meth:`Flask.errorhandler` but for a module. This - handler is used for all requests, even if outside of the module. - - .. versionadded:: 0.4 - """ - def decorator(f): - self._record(lambda s: s.app.errorhandler(code)(f)) - return f - return decorator - - def _record(self, func): - self._register_events.append(func) + if os.path.isdir(os.path.join(self.root_path, 'static')): + self._static_folder = 'static' diff --git a/flask/templating.py b/flask/templating.py index 8e785169..5800bf83 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -14,6 +14,7 @@ from jinja2 import BaseLoader, Environment as BaseEnvironment, \ from .globals import _request_ctx_stack from .signals import template_rendered +from .module import blueprint_is_module def _default_template_ctx_processor(): @@ -74,7 +75,9 @@ class DispatchingJinjaLoader(BaseLoader): loader = None try: module, name = posixpath.normpath(template).split('/', 1) - loader = self.app.modules[module].jinja_loader + blueprint = self.app.blueprints[module] + if blueprint_is_module(blueprint): + loader = blueprint.jinja_loader except (ValueError, KeyError, TemplateNotFound): pass try: diff --git a/flask/wrappers.py b/flask/wrappers.py index cc35e086..8db1ca9a 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -42,6 +42,10 @@ class Request(RequestBase): #: something similar. routing_exception = None + # switched by the request context until 1.0 to opt in deprecated + # module functionality + _is_old_module = False + @property def max_content_length(self): """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" @@ -61,17 +65,22 @@ class Request(RequestBase): @property def module(self): - """The name of the current module""" - if self.url_rule and \ - ':' not in self.url_rule.endpoint and \ - '.' in self.url_rule.endpoint: - return self.url_rule.endpoint.rsplit('.', 1)[0] + """The name of the current module if the request was dispatched + to an actual module. This is deprecated functionality, use blueprints + instead. + """ + from warnings import warn + warn(DeprecationWarning('modules were deprecated in favor of ' + 'blueprints. Use request.blueprint ' + 'instead.'), stacklevel=2) + if self._is_old_module: + return self.blueprint @property def blueprint(self): """The name of the current blueprint""" - if self.url_rule and ':' in self.url_rule.endpoint: - return self.url_rule.endpoint.split(':', 1)[0] + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.split('.', 1)[0] @cached_property def json(self): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 8530df8a..2abf1336 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1192,6 +1192,7 @@ class ModuleTestCase(unittest.TestCase): from flask import Module app = flask.Flask(__name__) + app.testing = True app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index') From f7e71b518f30923f494750e476f30d91c415723e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 18:55:06 +0200 Subject: [PATCH 0598/3747] Register most stuff only once --- flask/app.py | 4 +++- flask/blueprints.py | 39 +++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/flask/app.py b/flask/app.py index 2a83a020..fe84e695 100644 --- a/flask/app.py +++ b/flask/app.py @@ -583,6 +583,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ + first_registration = False if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint\'s name collision ocurred between %r and ' \ @@ -590,7 +591,8 @@ class Flask(_PackageBoundObject): (blueprint, self.blueprints[blueprint.name], blueprint.name) else: self.blueprints[blueprint.name] = blueprint - blueprint.register(self, options) + first_registration = True + blueprint.register(self, options, first_registration) def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` diff --git a/flask/blueprints.py b/flask/blueprints.py index 96a97d30..c335bf5a 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for more details. """ import os +from functools import update_wrapper from .helpers import _PackageBoundObject, _endpoint_from_view_func @@ -19,10 +20,11 @@ class BlueprintSetupState(object): application. """ - def __init__(self, blueprint, app, options): + def __init__(self, blueprint, app, options, first_registration): self.app = app self.blueprint = blueprint self.options = options + self.first_registration = first_registration subdomain = self.options.get('subdomain') if subdomain is None: @@ -64,17 +66,23 @@ class Blueprint(_PackageBoundObject): def _record(self, func): self.deferred_functions.append(func) - def make_setup_state(self, app, options): - return BlueprintSetupState(self, app, options) + def _record_once(self, func): + def wrapper(state): + if state.first_registration: + func(state) + return self._record(update_wrapper(wrapper, func)) - def register(self, app, options): + def make_setup_state(self, app, options, first_registration=False): + return BlueprintSetupState(self, app, options, first_registration) + + def register(self, app, options, first_registration=False): """Called by :meth:`Flask.register_blueprint` to register a blueprint on the application. This can be overridden to customize the register behavior. Keyword arguments from :func:`~flask.Flask.register_blueprint` are directly forwarded to this method in the `options` dictionary. """ - state = self.make_setup_state(app, options) + state = self.make_setup_state(app, options, first_registration) if self.has_static_folder: state.add_url_rule(self.static_url_path + '/', view_func=self.send_static_file, @@ -96,9 +104,8 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. """ - def register_rule(state): - state.add_url_rule(rule, endpoint, view_func, **options) - self._record(register_rule) + self._record(lambda s: + s.add_url_rule(rule, endpoint, view_func, **options)) def endpoint(self, endpoint): """Like :meth:`Flask.endpoint` but for a module. This does not @@ -108,7 +115,7 @@ class Blueprint(_PackageBoundObject): def decorator(f): def register_endpoint(state): state.app.view_functions[endpoint] = f - self._record(register_endpoint) + self._record_once(register_endpoint) return f return decorator @@ -117,7 +124,7 @@ class Blueprint(_PackageBoundObject): is only executed before each request that is handled by a function of that module. """ - self._record(lambda s: s.app.before_request_funcs + self._record_once(lambda s: s.app.before_request_funcs .setdefault(self.name, []).append(f)) return f @@ -125,7 +132,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a module. """ - self._record(lambda s: s.app.before_request_funcs + self._record_once(lambda s: s.app.before_request_funcs .setdefault(None, []).append(f)) return f @@ -134,7 +141,7 @@ class Blueprint(_PackageBoundObject): is only executed after each request that is handled by a function of that module. """ - self._record(lambda s: s.app.after_request_funcs + self._record_once(lambda s: s.app.after_request_funcs .setdefault(self.name, []).append(f)) return f @@ -142,7 +149,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.after_request` but for a module. Such a function is executed after each request, even if outside of the module. """ - self._record(lambda s: s.app.after_request_funcs + self._record_once(lambda s: s.app.after_request_funcs .setdefault(None, []).append(f)) return f @@ -150,7 +157,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.context_processor` but for a module. This function is only executed for requests handled by a module. """ - self._record(lambda s: s.app.template_context_processors + self._record_once(lambda s: s.app.template_context_processors .setdefault(self.name, []).append(f)) return f @@ -158,7 +165,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.context_processor` but for a module. Such a function is executed each request, even if outside of the module. """ - self._record(lambda s: s.app.template_context_processors + self._record_once(lambda s: s.app.template_context_processors .setdefault(None, []).append(f)) return f @@ -167,6 +174,6 @@ class Blueprint(_PackageBoundObject): handler is used for all requests, even if outside of the module. """ def decorator(f): - self._record(lambda s: s.app.errorhandler(code)(f)) + self._record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator From 5da1fc22153032923b1560a34a0f346d6517a12d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 19:52:10 +0200 Subject: [PATCH 0599/3747] Started work on URL value preprocessors --- flask/app.py | 50 ++++++++++++++++++++++++++++++++++++++++++++- flask/blueprints.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index fe84e695..9ac6319c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -269,6 +269,28 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 self.teardown_request_funcs = {} + #: A dictionary with lists of functions that can be used as URL + #: value processor functions. Whenever a URL is built these functions + #: are called to modify the dictionary of values in place. The key + #: `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: + #: .. versionadded:: 0.7 + self.url_value_preprocessors = {} + + #: A dictionary with lists of functions that can be used as URL value + #: preprocessors. The key `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: of URL values before they are used as the keyword arguments of the + #: view function. For each function registered this one should also + #: provide a :meth:`url_defaults` function that adds the parameters + #: automatically again that were removed that way. + #: + #: .. versionadded:: 0.7 + self.url_default_functions = {} + #: A dictionary with list of functions that are called without argument #: to populate the template context. The key of the dictionary is the #: name of the blueprint this function is active for, `None` for all @@ -826,6 +848,22 @@ class Flask(_PackageBoundObject): self.template_context_processors[None].append(f) return f + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for all view + functions of the application. It's called before the view functions + are called and can modify the url values provided. + """ + self.url_value_preprocessors.setdefault(None, []).append(f) + return f + + def url_defaults(self, f): + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + """ + self.url_default_functions.setdefault(None, []).append(f) + return f + def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the @@ -981,9 +1019,19 @@ class Flask(_PackageBoundObject): If any of these function returns a value it's handled as if it was the return value from the view and further request handling is stopped. + + This also triggers the :meth:`url_value_processor` functions before + the actualy :meth:`before_request` functions are called. """ - funcs = self.before_request_funcs.get(None, ()) bp = request.blueprint + + funcs = self.url_value_preprocessors.get(None, ()) + if bp is not None and bp in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[bp]) + for func in funcs: + func(request.endpoint, request.values) + + funcs = self.before_request_funcs.get(None, ()) if bp is not None and bp in self.before_request_funcs: funcs = chain(funcs, self.before_request_funcs[bp]) for func in funcs: diff --git a/flask/blueprints.py b/flask/blueprints.py index c335bf5a..b804c316 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -177,3 +177,35 @@ class Blueprint(_PackageBoundObject): self._record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator + + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for this + blueprint. It's called before the view functions are called and + can modify the url values provided. + """ + self._record_once(lambda s: s.app.url_value_preprocessors + .setdefault(self.name, []).append(f)) + return f + + def url_defaults(self, f): + """Callback function for URL defaults for this module. It's called + with the endpoint and values and should update the values passed + in place. + """ + self._record_once(lambda s: s.app.url_default_functions + .setdefault(self.name, []).append(f)) + return f + + def app_url_value_preprocessor(self, f): + """Same as :meth:`url_value_preprocessor` but application wide. + """ + self._record_once(lambda s: s.app.url_value_preprocessor + .setdefault(self.name, []).append(f)) + return f + + def app_url_defaults(self, f): + """Same as :meth:`url_defaults` but application wide. + """ + self._record_once(lambda s: s.app.url_default_functions + .setdefault(None, []).append(f)) + return f From 4d6cd1a3901495f66513934bb7e1a6ac58116400 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 19:56:16 +0200 Subject: [PATCH 0600/3747] URL building should work in theory --- flask/app.py | 14 ++++++++++++++ flask/helpers.py | 1 + 2 files changed, 15 insertions(+) diff --git a/flask/app.py b/flask/app.py index 9ac6319c..662d6ca0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1013,6 +1013,20 @@ class Flask(_PackageBoundObject): return self.url_map.bind_to_environ(request.environ, server_name=self.config['SERVER_NAME']) + def inject_url_defaults(self, endpoint, values): + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + funcs = self.url_default_functions.get(None, ()) + if '.' in endpoint: + bp = endpoint.split('.', 1)[0] + funcs = chain(funcs, self.url_default_functions.get(bp, ())) + for func in funcs: + func(endpoint, values) + def preprocess_request(self): """Called before the actual request dispatching and will call every as :meth:`before_request` decorated function. diff --git a/flask/helpers.py b/flask/helpers.py index a2afa0f6..14521ce1 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -180,6 +180,7 @@ def url_for(endpoint, **values): elif endpoint.startswith('.'): endpoint = endpoint[1:] external = values.pop('_external', False) + ctx.app.inject_url_defaults(endpoint, values) return ctx.url_adapter.build(endpoint, values, force_external=external) From db28217574c119b65f4a5c9dcc7922656d60f22b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 20:05:39 +0200 Subject: [PATCH 0601/3747] Fixed a bug, view_args not values --- flask/__init__.py | 1 + flask/app.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index f9fcc09f..03f91baa 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -23,6 +23,7 @@ from .helpers import url_for, jsonify, json_available, flash, \ from .globals import current_app, g, request, session, _request_ctx_stack from .ctx import has_request_context from .module import Module +from .blueprints import Blueprint from .templating import render_template, render_template_string from .session import Session diff --git a/flask/app.py b/flask/app.py index 662d6ca0..e8d2672f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1043,7 +1043,7 @@ class Flask(_PackageBoundObject): if bp is not None and bp in self.url_value_preprocessors: funcs = chain(funcs, self.url_value_preprocessors[bp]) for func in funcs: - func(request.endpoint, request.values) + func(request.endpoint, request.view_args) funcs = self.before_request_funcs.get(None, ()) if bp is not None and bp in self.before_request_funcs: From 8017febac4d8cfec57378a6d5514ab19957f8cdc Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 30 May 2011 15:06:32 +0200 Subject: [PATCH 0602/3747] Don't fail for unicode filenames with send_file(add_etags=True) --- flask/helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index a25dcadd..806ff0ee 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -379,7 +379,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, rv.set_etag('flask-%s-%s-%s' % ( os.path.getmtime(filename), os.path.getsize(filename), - adler32(filename) & 0xffffffff + adler32( + filename.encode('utf8') if isinstance(filename, unicode) + else filename + ) & 0xffffffff )) if conditional: rv = rv.make_conditional(request) From 9beee51b0b1c190a90bdfae946a66ca0dddf0631 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 30 May 2011 21:32:01 +0200 Subject: [PATCH 0603/3747] Clarify in docs that the SERVER_NAME config can also take a port number. --- docs/config.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 90a276cc..50aa5884 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -65,8 +65,9 @@ The following configuration values are used internally by Flask: :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile ``LOGGER_NAME`` the name of the logger -``SERVER_NAME`` the name of the server. Required for - subdomain support (e.g.: ``'localhost'``) +``SERVER_NAME`` the name and port number of the server. + Required for subdomain support (e.g.: + ``'localhost:5000'``) ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by From f5ec9952decda8731a1f40ba788ba06d12c0229b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 10:27:15 +0200 Subject: [PATCH 0604/3747] Added blueprint specific error handling --- CHANGES | 5 +++ flask/app.py | 105 ++++++++++++++++++++++++++++++++++++++----- flask/blueprints.py | 24 +++++++++- tests/flask_tests.py | 60 +++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 12 deletions(-) diff --git a/CHANGES b/CHANGES index 92a147b0..3a367ead 100644 --- a/CHANGES +++ b/CHANGES @@ -51,6 +51,11 @@ Release date to be announced, codename to be selected - Don't modify the session on :func:`flask.get_flashed_messages` if there are no messages in the session. - `before_request` handlers are now able to abort requests with errors. +- it is not possible to define user exception handlers. That way you can + provide custom error messages from a central hub for certain errors that + might occur during request processing (for instance database connection + errors, timeouts from remote resources etc.). +- Blueprints can provide blueprint specific error handlers. Version 0.6.1 ------------- diff --git a/flask/app.py b/flask/app.py index e8d2672f..8f371dba 100644 --- a/flask/app.py +++ b/flask/app.py @@ -234,12 +234,21 @@ class Flask(_PackageBoundObject): #: To register a view function, use the :meth:`route` decorator. self.view_functions = {} - #: A dictionary of all registered error handlers. The key is - #: be the error code as integer, the value the function that - #: should handle that error. + # support for the now deprecated `error_handlers` attribute. The + # :attr:`error_handler_spec` shall be used now. + self._error_handlers = {} + + #: A dictionary of all registered error handlers. The key is `None` + #: for error handlers active on the application, otherwise the key is + #: the name of the blueprint. Each key points to another dictionary + #: where they key is the status code of the http exception. The + #: special key `None` points to a list of tuples where the first item + #: is the class for the instance check and the second the error handler + #: function. + #: #: To register a error handler, use the :meth:`errorhandler` #: decorator. - self.error_handlers = {} + self.error_handler_spec = {None: self._error_handlers} #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of @@ -351,6 +360,17 @@ class Flask(_PackageBoundObject): endpoint='static', view_func=self.send_static_file) + def _get_error_handlers(self): + from warnings import warn + warn(DeprecationWarning('error_handlers is deprecated, use the ' + 'new error_handler_spec attribute instead.'), stacklevel=1) + return self._error_handlers + def _set_error_handlers(self, value): + self._error_handlers = value + self.error_handler_spec[None] = value + error_handlers = property(_get_error_handlers, _set_error_handlers) + del _get_error_handlers, _set_error_handlers + @property def propagate_exceptions(self): """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration @@ -761,7 +781,7 @@ class Flask(_PackageBoundObject): return f return decorator - def errorhandler(self, code): + def errorhandler(self, code_or_exception): """A decorator that is used to register a function give a given error code. Example:: @@ -769,21 +789,51 @@ class Flask(_PackageBoundObject): def page_not_found(error): return 'This page does not exist', 404 + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + You can also register a function as error handler without using the :meth:`errorhandler` decorator. The following example is equivalent to the one above:: def page_not_found(error): return 'This page does not exist', 404 - app.error_handlers[404] = page_not_found + app.error_handler_spec[None][404] = page_not_found + + Setting error handlers via assignments to :attr:`error_handler_spec` + however is discouraged as it requires fidling with nested dictionaries + and the special case for arbitrary exception types. + + The first `None` refers to the active blueprint. If the error + handler should be application wide `None` shall be used. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:~`werkzeug.exceptions.HTTPException` class. :param code: the code as integer for the handler """ def decorator(f): - self.error_handlers[code] = f + self._register_error_handler(None, code_or_exception, f) return f return decorator + def _register_error_handler(self, key, code_or_exception, f): + if isinstance(code_or_exception, HTTPException): + code_or_exception = code_or_exception.code + if isinstance(code_or_exception, (int, long)): + assert code_or_exception != 500 or key is None, \ + 'It is currently not possible to register a 500 internal ' \ + 'server error on a per-blueprint level.' + self.error_handler_spec.setdefault(key, {})[code_or_exception] = f + else: + self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ + .append((code_or_exception, f)) + def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function @@ -871,11 +921,44 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ - handler = self.error_handlers.get(e.code) + handlers = self.error_handler_spec.get(request.blueprint) + if handlers and e.code in handlers: + handler = handlers[e.code] + else: + handler = self.error_handler_spec[None].get(e.code) if handler is None: return e return handler(e) + def handle_user_exception(self, e): + """This method is called whenever an exception occurs that should be + handled. A special case are + :class:`~werkzeug.exception.HTTPException`\s which are forwarded by + this function to the :meth:`handle_http_exception` method. This + function will either return a response value or reraise the + exception with the same traceback. + + .. versionadded:: 0.7 + """ + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. + if isinstance(e, HTTPException): + return self.handle_http_exception(e) + + exc_type, exc_value, tb = sys.exc_info() + assert exc_value is e + + blueprint_handlers = () + handlers = self.error_handler_spec.get(request.blueprint) + if handlers is not None: + blueprint_handlers = handlers.get(None, ()) + app_handlers = self.error_handler_spec[None].get(None, ()) + for typecheck, handler in chain(blueprint_handlers, app_handlers): + if isinstance(e, typecheck): + return handler(e) + + raise exc_type, exc_value, tb + def handle_exception(self, e): """Default exception handling that kicks in when an exception occours that is not caught. In debug mode the exception will @@ -888,7 +971,7 @@ class Flask(_PackageBoundObject): exc_type, exc_value, tb = sys.exc_info() got_request_exception.send(self, exception=e) - handler = self.error_handlers.get(500) + handler = self.error_handler_spec[None].get(500) if self.propagate_exceptions: # if we want to repropagate the exception, we can attempt to @@ -942,8 +1025,8 @@ class Flask(_PackageBoundObject): rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() - except HTTPException, e: - rv = self.handle_http_exception(e) + except Exception, e: + rv = self.handle_user_exception(e) response = self.make_response(rv) response = self.process_response(response) request_finished.send(self, response=response) diff --git a/flask/blueprints.py b/flask/blueprints.py index b804c316..1fed639d 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -62,6 +62,7 @@ class Blueprint(_PackageBoundObject): self.static_folder = static_folder self.static_url_path = static_url_path self.deferred_functions = [] + self.view_functions = {} def _record(self, func): self.deferred_functions.append(func) @@ -110,7 +111,9 @@ class Blueprint(_PackageBoundObject): def endpoint(self, endpoint): """Like :meth:`Flask.endpoint` but for a module. This does not prefix the endpoint with the module name, this has to be done - explicitly by the user of this method. + explicitly by the user of this method. If the endpoint is prefixed + with a `.` it will be registered to the current blueprint, otherwise + it's an application independent endpoint. """ def decorator(f): def register_endpoint(state): @@ -209,3 +212,22 @@ class Blueprint(_PackageBoundObject): self._record_once(lambda s: s.app.url_default_functions .setdefault(None, []).append(f)) return f + + def errorhandler(self, code_or_exception): + """Registers an error handler that becomes active for this blueprint + only. Please be aware that routing does not happen local to a + blueprint so an error handler for 404 usually is not handled by + a blueprint unless it is caused inside a view function. Another + special case is the 500 internal server error which is always looked + up from the application. + + Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator + of the :class:`~flask.Flask` object. + + .. versionadded:: 0.7 + """ + def decorator(f): + self._record_once(lambda s: s.app._register_error_handler( + self.name, code_or_exception, f)) + return f + return decorator diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2abf1336..7c430417 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -531,6 +531,22 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_user_error_handling(self): + class MyException(Exception): + pass + + app = flask.Flask(__name__) + @app.errorhandler(MyException) + def handle_my_exception(e): + assert isinstance(e, MyException) + return '42' + @app.route('/') + def index(): + raise MyException() + + c = app.test_client() + assert c.get('/').data == '42' + def test_teardown_on_pop(self): buffer = [] app = flask.Flask(__name__) @@ -1214,6 +1230,49 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/foo/bar').data == 'bar' +class BlueprintTestCase(unittest.TestCase): + + def test_blueprint_specific_error_handling(self): + frontend = flask.Blueprint('frontend', __name__) + backend = flask.Blueprint('backend', __name__) + sideend = flask.Blueprint('sideend', __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return 'frontend says no', 403 + + @frontend.route('/frontend-no') + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return 'backend says no', 403 + + @backend.route('/backend-no') + def backend_no(): + flask.abort(403) + + @sideend.route('/what-is-a-sideend') + def sideend_no(): + flask.abort(403) + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return 'application itself says no', 403 + + c = app.test_client() + + assert c.get('/frontend-no').data == 'frontend says no' + assert c.get('/backend-no').data == 'backend says no' + assert c.get('/what-is-a-sideend').data == 'application itself says no' + + class SendfileTestCase(unittest.TestCase): def test_send_file_regular(self): @@ -1631,6 +1690,7 @@ def suite(): suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) suite.addTest(unittest.makeSuite(TemplatingTestCase)) suite.addTest(unittest.makeSuite(ModuleTestCase)) + suite.addTest(unittest.makeSuite(BlueprintTestCase)) suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) From 1f31ec4bea20e67f1870a0857c869eafa109c4d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 10:32:00 +0200 Subject: [PATCH 0605/3747] Added documentation in update document for new error handling --- docs/upgrading.rst | 27 +++++++++++++++++++++++++++ flask/app.py | 9 +++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7d6e0f0a..903524ca 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -91,6 +91,33 @@ handling:: # and here it's gone +Manual Error Handler Attaching +`````````````````````````````` + +While it is still possible to attach error handlers to +:attr:`Flask.error_handlers` it's discouraged to do so and in fact +deprecated. In generaly we no longer recommend custom error handler +attaching via assignments to the underlying dictionary due to the more +complex internal handling to support arbitrary exception classes and +blueprints. See :meth:`Flask.errorhandler` for more information. + +The proper upgrade is to change this:: + + app.error_handlers[403] = handle_error + +Into this:: + + app.register_error_handler(403, handle_error) + +Alternatively you should just attach the function with a decorator:: + + @app.errorhandler(403) + def handle_error(e): + ... + +(Note that :meth:`register_error_handler` is new in Flask 0.7) + + Version 0.6 ----------- diff --git a/flask/app.py b/flask/app.py index 8f371dba..b76e69e6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -822,6 +822,15 @@ class Flask(_PackageBoundObject): return f return decorator + def register_error_handler(self, code_or_exception, f): + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + self._register_error_handler(None, code_or_exception, f) + def _register_error_handler(self, key, code_or_exception, f): if isinstance(code_or_exception, HTTPException): code_or_exception = code_or_exception.code From cb604e39bb3cc06a2c45a72dc6100d2aef191e76 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 5 Jun 2011 04:38:24 -0400 Subject: [PATCH 0606/3747] Approve Frozen-Flask. --- extreview/approved.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 58d18e72..3a21d496 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -144,3 +144,14 @@ Flask-XML-RPC :Approved License: MIT All fine. + + +Frozen-Flask +------------ + +:First Approval: 2011-06-05 +:Last Review: 2011-06-05 +:Approved Version: 0.4 +:Approved License: BSD + +All fine. Posted recommendations for minor items/enhancements to mailing list. From 6dae36f94db40d242fee73d10467a25cfa90b580 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 13:19:28 +0200 Subject: [PATCH 0607/3747] Started work on an upgrade script for blueprints --- flask/helpers.py | 11 ++- scripts/flask-07-upgrade.py | 171 ++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 scripts/flask-07-upgrade.py diff --git a/flask/helpers.py b/flask/helpers.py index 14521ce1..eff32273 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -168,15 +168,18 @@ def url_for(endpoint, **values): :param _external: if set to `True`, an absolute URL is generated. """ ctx = _request_ctx_stack.top + blueprint_name = request.blueprint if not ctx.request._is_old_module: if endpoint[:1] == '.': - endpoint = request.blueprint + endpoint + if blueprint_name is not None: + endpoint = blueprint_name + endpoint + else: + endpoint = endpoint[1:] else: # TODO: get rid of this deprecated functionality in 1.0 if '.' not in endpoint: - mod = ctx.request.blueprint - if mod is not None: - endpoint = mod + '.' + endpoint + if blueprint_name is not None: + endpoint = blueprint_name + '.' + endpoint elif endpoint.startswith('.'): endpoint = endpoint[1:] external = values.pop('_external', False) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py new file mode 100644 index 00000000..02eb3faf --- /dev/null +++ b/scripts/flask-07-upgrade.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + flask-07-upgrade + ~~~~~~~~~~~~~~~~ + + This command line script scans a whole application tree and attempts to + output an unified diff with all the changes that are necessary to easily + upgrade the application to 0.7 and to not yield deprecation warnings. + + This will also attempt to find `after_request` functions that don't modify + the response and appear to be better suited for `teardown_request`. + + :copyright: (c) Copyright 2011 by Armin Ronacher. + :license: see LICENSE for more details. +""" +import re +import os +import sys +import inspect +import difflib +import posixpath +from optparse import OptionParser + +try: + import ast +except ImportError: + ast = None + + +_string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ + r'|"([^"\\]*(?:\\.[^"\\]*)*)")' +_url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) +_after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') + + +def error(message): + print >> sys.stderr, 'error:', message + sys.exit(1) + + +def make_diff(filename, old, new): + for line in difflib.unified_diff(old.splitlines(), new.splitlines(), + posixpath.normpath(posixpath.join('a', filename)), + posixpath.normpath(posixpath.join('b', filename)), + lineterm=''): + print line + + +def fix_url_for(contents): + def handle_match(match): + prefix = match.group(1) + endpoint = ast.literal_eval(match.group(2)) + if endpoint.startswith('.'): + endpoint = endpoint[1:] + else: + endpoint = '.' + endpoint + return prefix + repr(endpoint) + return _url_for_re.sub(handle_match, contents) + + +def looks_like_teardown_function(node): + returns = [x for x in ast.walk(node) if isinstance(x, ast.Return)] + if len(returns) != 1: + return + return_def = returns[0] + resp_name = node.args.args[0] + if not isinstance(return_def.value, ast.Name) or \ + return_def.value.id != resp_name.id: + return + + for body_node in node.body: + for child in ast.walk(body_node): + if isinstance(child, ast.Name) and \ + child.id == resp_name.id: + if child is not return_def.value: + return + + return resp_name.id + + +def fix_teardown_funcs(contents): + + def is_return_line(line): + args = line.strip().split() + return args and args[0] == 'return' + + def fix_single(match, lines, lineno): + if not lines[lineno + 1].startswith('def'): + return + block_lines = inspect.getblock(lines[lineno + 1:]) + func_code = ''.join(block_lines) + if func_code[0].isspace(): + node = ast.parse('if 1:\n' + func_code).body[0].body + else: + node = ast.parse(func_code).body[0] + response_param_name = looks_like_teardown_function(node) + if response_param_name is None: + return + before = lines[:lineno] + decorator = [match.group(1) + + match.group(2).replace('after_', 'teardown_') + + match.group(3)] + body = [line.replace(response_param_name, 'exception') + for line in block_lines if + not is_return_line(line)] + after = lines[lineno + len(block_lines) + 1:] + return before + decorator + body + after + + content_lines = contents.splitlines(True) + while 1: + found_one = False + for idx, line in enumerate(content_lines): + match = _after_request_re.match(line) + if match is None: + continue + new_content_lines = fix_single(match, content_lines, idx) + if new_content_lines is not None: + content_lines = new_content_lines + break + else: + break + + return ''.join(content_lines) + + +def upgrade_python_file(filename, contents, teardown): + new_contents = fix_url_for(contents) + if teardown: + new_contents = fix_teardown_funcs(contents) + make_diff(filename, contents, new_contents) + + +def upgrade_template_file(filename, contents): + new_contents = fix_url_for(contents) + make_diff(filename, contents, new_contents) + + +def scan_path(path=None, teardown=True): + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + filename = os.path.join(dirpath, filename) + with open(filename) as f: + contents = f.read() + if filename.endswith('.py'): + upgrade_python_file(filename, contents, teardown) + elif '{% for' or '{% if' or '{{ url_for' in contents: + upgrade_template_file(filename, contents) + + +def main(): + """Entrypoint""" + if ast is None: + error('Python 2.6 or later is required to run the upgrade script.\n' + 'The runtime requirements for Flask 0.7 however are still ' + 'Python 2.5.') + + parser = OptionParser() + parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', + action='store_true', help='Do not attempt to ' + 'detect teardown function rewrites.') + options, args = parser.parse_args() + if not args: + args = ['.'] + + for path in args: + scan_path(path, teardown=not options.no_teardown) + + +if __name__ == '__main__': + main() From 5e91b0c352d6493b2669c6da413fd0ed65cb6b7c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 13:52:40 +0200 Subject: [PATCH 0608/3747] Import and some usage rewriting. --- scripts/flask-07-upgrade.py | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 02eb3faf..87ee5614 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -28,10 +28,18 @@ except ImportError: ast = None +_from_import_re = re.compile(r'^\s*from flask import\s+') +_direct_module_usage_re = re.compile(r'flask\.Module') _string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ r'|"([^"\\]*(?:\\.[^"\\]*)*)")' _url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) _after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') +_module_constructor_re = re.compile(r'Module\(__name__\s*(?:,\s*(%s))?' % + _string_re_part) +_blueprint_related = [ + (re.compile(r'request\.module'), 'request.blueprint'), + (re.compile(r'register_module'), 'register_blueprint') +] def error(message): @@ -124,10 +132,66 @@ def fix_teardown_funcs(contents): return ''.join(content_lines) +def get_module_autoname(filename): + directory, filename = os.path.split(filename) + if filename != '__init__.py': + return os.path.splitext(filename)[0] + return os.path.basename(directory) + + +def rewrite_from_imports(prefix, fromlist, lineiter): + import_block = [prefix, fromlist] + if fromlist[0] == '(' and fromlist[-1] != ')': + for line in lineiter: + import_block.append(line) + if line.rstrip().endswith(')'): + break + elif fromlist[-1] == '\\': + for line in lineiter: + import_block.append(line) + if line.rstrip().endswith('\\'): + break + + return ''.join(import_block).replace('Module', 'Blueprint') + + +def rewrite_blueprint_imports(contents): + new_file = [] + lineiter = iter(contents.splitlines(True)) + for line in lineiter: + match = _from_import_re.search(line) + if match is not None: + new_file.extend(rewrite_from_imports(match.group(), + line[match.end():], + lineiter)) + continue + new_file.append(_direct_module_usage_re.sub('flask.Blueprint', line)) + return ''.join(new_file) + + +def rewrite_for_blueprints(contents, filename): + found_constructor = [] + def handle_match(match): + found_constructor[:] = [True] + name_param = match.group(1) + if name_param is None: + return 'Blueprint(%r, __name__' % get_module_autoname(filename) + return 'Blueprint(%s, __name__' % name_param + new_contents = _module_constructor_re.sub(handle_match, contents) + + if found_constructor: + new_contents = rewrite_blueprint_imports(new_contents) + + for pattern, replacement in _blueprint_related: + new_contents = pattern.sub(replacement, new_contents) + return new_contents + + def upgrade_python_file(filename, contents, teardown): new_contents = fix_url_for(contents) if teardown: - new_contents = fix_teardown_funcs(contents) + new_contents = fix_teardown_funcs(new_contents) + new_contents = rewrite_for_blueprints(new_contents, filename) make_diff(filename, contents, new_contents) From 479e242afc0665306745ea1845ba47ceaaede770 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 13:53:40 +0200 Subject: [PATCH 0609/3747] Direct rewrites are unnecessary, constructor calls are enough --- scripts/flask-07-upgrade.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 87ee5614..3ae0eaef 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -29,7 +29,6 @@ except ImportError: _from_import_re = re.compile(r'^\s*from flask import\s+') -_direct_module_usage_re = re.compile(r'flask\.Module') _string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ r'|"([^"\\]*(?:\\.[^"\\]*)*)")' _url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) @@ -164,8 +163,8 @@ def rewrite_blueprint_imports(contents): new_file.extend(rewrite_from_imports(match.group(), line[match.end():], lineiter)) - continue - new_file.append(_direct_module_usage_re.sub('flask.Blueprint', line)) + else: + new_file.append(line) return ''.join(new_file) From 6d072f00adb12569380becd17f277b5456ad07c0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 14:31:17 +0200 Subject: [PATCH 0610/3747] Better control over template rendering calls --- scripts/flask-07-upgrade.py | 117 ++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 3ae0eaef..a91db85c 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -32,9 +32,12 @@ _from_import_re = re.compile(r'^\s*from flask import\s+') _string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ r'|"([^"\\]*(?:\\.[^"\\]*)*)")' _url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) +_render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part) _after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') -_module_constructor_re = re.compile(r'Module\(__name__\s*(?:,\s*(%s))?' % +_module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' + r'\(__name__\s*(?:,\s*(%s))?' % _string_re_part) +_mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') _blueprint_related = [ (re.compile(r'request\.module'), 'request.blueprint'), (re.compile(r'register_module'), 'register_blueprint') @@ -168,29 +171,66 @@ def rewrite_blueprint_imports(contents): return ''.join(new_file) -def rewrite_for_blueprints(contents, filename): - found_constructor = [] +def rewrite_render_template_calls(contents, module_declarations): + mapping = dict(module_declarations) + annotated_lines = [] + + def make_line_annotations(): + if not annotated_lines: + last_index = 0 + for line in contents.splitlines(True): + last_index += len(line) + annotated_lines.append((last_index, line)) + + def backtrack_module_name(call_start): + make_line_annotations() + for idx, (line_end, line) in enumerate(annotated_lines): + if line_end > call_start: + for _, line in reversed(annotated_lines[:idx]): + match = _mod_route_re.search(line) + if match is not None: + shortname = match.group(1) + return mapping.get(shortname) + def handle_match(match): - found_constructor[:] = [True] - name_param = match.group(1) + template_name = ast.literal_eval(match.group(2)) + modname = backtrack_module_name(match.start()) + if modname is not None and template_name.startswith(modname + '/'): + template_name = modname + ':' + template_name[len(modname) + 1:] + return match.group(1) + repr(template_name) + return _render_template_re.sub(handle_match, contents) + + +def rewrite_for_blueprints(contents, filename, template_bundles=False): + modules_declared = [] + def handle_match(match): + target = match.group(1) + name_param = match.group(2) if name_param is None: - return 'Blueprint(%r, __name__' % get_module_autoname(filename) - return 'Blueprint(%s, __name__' % name_param + modname = get_module_autoname(filename) + else: + modname = ast.literal_eval(name_param) + modules_declared.append((target, modname)) + return '%s = %s' % (target, 'Blueprint(%r, __name__' % modname) new_contents = _module_constructor_re.sub(handle_match, contents) - if found_constructor: + if modules_declared: new_contents = rewrite_blueprint_imports(new_contents) + if template_bundles: + new_contents = rewrite_render_template_calls(new_contents, + modules_declared) for pattern, replacement in _blueprint_related: new_contents = pattern.sub(replacement, new_contents) return new_contents -def upgrade_python_file(filename, contents, teardown): +def upgrade_python_file(filename, contents, teardown, template_bundles): new_contents = fix_url_for(contents) if teardown: new_contents = fix_teardown_funcs(new_contents) - new_contents = rewrite_for_blueprints(new_contents, filename) + new_contents = rewrite_for_blueprints(new_contents, filename, + template_bundles) make_diff(filename, contents, new_contents) @@ -199,16 +239,43 @@ def upgrade_template_file(filename, contents): make_diff(filename, contents, new_contents) -def scan_path(path=None, teardown=True): +def walk_path(path): for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: filename = os.path.join(dirpath, filename) - with open(filename) as f: - contents = f.read() if filename.endswith('.py'): - upgrade_python_file(filename, contents, teardown) - elif '{% for' or '{% if' or '{{ url_for' in contents: - upgrade_template_file(filename, contents) + yield filename, 'python' + else: + with open(filename) as f: + contents = f.read() + if '{% for' or '{% if' or '{{ url_for' in contents: + yield filename, 'template' + + +def scan_path(path=None, teardown=True, template_bundles=True): + for filename, type in walk_path(path): + with open(filename) as f: + contents = f.read() + if type == 'python': + upgrade_python_file(filename, contents, teardown, + template_bundles) + elif type == 'template': + upgrade_template_file(filename, contents) + + +def autodetect_template_bundles(paths): + folders_with_templates = set() + for path in paths: + for filename, type in walk_path(path): + if type == 'template': + fullpath = filename.replace(os.path.sep, '/') + index = fullpath.find('/templates/') + if index >= 0: + folder = fullpath[:index] + else: + folder = posixpath.dirname(fullpath) + folders_with_templates.add(folder) + return len(folders_with_templates) > 1 def main(): @@ -222,12 +289,28 @@ def main(): parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', action='store_true', help='Do not attempt to ' 'detect teardown function rewrites.') + parser.add_option('-b', '--bundled-templates', dest='bundled_tmpl', + action='store_true', help='Indicate to the system ' + 'that templates are bundled with modules. Default ' + 'is auto detect.') + parser.add_option('-B', '--no-bundled-templates', dest='no_bundled_tmpl', + action='store_true', help='Indicate to the system ' + 'that templates are not bundled with modules. ' + 'Default is auto detect') options, args = parser.parse_args() if not args: args = ['.'] + if options.no_bundled_tmpl: + template_bundles = False + elif options.bundled_tmpl: + template_bundles = True + else: + template_bundles = autodetect_template_bundles(args) + for path in args: - scan_path(path, teardown=not options.no_teardown) + scan_path(path, teardown=not options.no_teardown, + template_bundles=template_bundles) if __name__ == '__main__': From 670d2bcf7aaf6be3701aa58fb2ad39d3bb80e071 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 14:31:33 +0200 Subject: [PATCH 0611/3747] Added test project I use for testing --- scripts/testproj/templates/index.html | 1 + scripts/testproj/test.py | 30 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 scripts/testproj/templates/index.html create mode 100644 scripts/testproj/test.py diff --git a/scripts/testproj/templates/index.html b/scripts/testproj/templates/index.html new file mode 100644 index 00000000..12392e26 --- /dev/null +++ b/scripts/testproj/templates/index.html @@ -0,0 +1 @@ +{{ url_for('static', filename='test.css') }} diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py new file mode 100644 index 00000000..fb999628 --- /dev/null +++ b/scripts/testproj/test.py @@ -0,0 +1,30 @@ +from flask import Flask, Module + + +mod = Module(__name__) +mod2 = Module(__name__, 'testmod2') + +app = Flask(__name__) +app.register_module(mod) +app.register_module(mod2) + + +@app.after_request +def after_request(response): + g.db.close() + return response + + +@mod.route('/') +def index(): + return render_template('test/index.html') + + +@mod2.route('/') +def index(): + return render_template('testmod2/index.html') + + +@mod2.route('/') +def index(): + return render_template('something-else/index.html') From cbfacd8962587d864e89d876e772fb4c1234f94d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 14:32:33 +0200 Subject: [PATCH 0612/3747] Added disclaimer --- scripts/flask-07-upgrade.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index a91db85c..abe774c5 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -11,6 +11,11 @@ This will also attempt to find `after_request` functions that don't modify the response and appear to be better suited for `teardown_request`. + This application is indeed an incredible hack, but because what it + attempts to accomplish is impossible to do statically it tries to support + the most common patterns at least. The diff it generates should be + hand reviewed and not applied blindly without making backups. + :copyright: (c) Copyright 2011 by Armin Ronacher. :license: see LICENSE for more details. """ From 10104e98cf89b1e57f8359b0209307fefaee2ea1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 14:37:33 +0200 Subject: [PATCH 0613/3747] app.modules -> app.blueprints --- scripts/flask-07-upgrade.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index abe774c5..ab530e5e 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -45,7 +45,8 @@ _module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' _mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') _blueprint_related = [ (re.compile(r'request\.module'), 'request.blueprint'), - (re.compile(r'register_module'), 'register_blueprint') + (re.compile(r'register_module'), 'register_blueprint'), + (re.compile(r'app\.modules'), 'app.blueprints') ] From 17d1ade6b956a1dc508f096f951fc52800b2a20e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 14:37:57 +0200 Subject: [PATCH 0614/3747] Alternative names for app --- scripts/flask-07-upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index ab530e5e..5398584e 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -46,7 +46,7 @@ _mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') _blueprint_related = [ (re.compile(r'request\.module'), 'request.blueprint'), (re.compile(r'register_module'), 'register_blueprint'), - (re.compile(r'app\.modules'), 'app.blueprints') + (re.compile(r'(app|application)\.modules'), '\\1.blueprints') ] From aa863efd9294dcd8afef6e5fa590ae2f88cbb0a2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:00:19 +0200 Subject: [PATCH 0615/3747] Don't scan the whole file contents for template detection --- scripts/flask-07-upgrade.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 5398584e..216db1bf 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -33,6 +33,9 @@ except ImportError: ast = None +TEMPLATE_LOOKAHEAD = 4096 + + _from_import_re = re.compile(r'^\s*from flask import\s+') _string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ r'|"([^"\\]*(?:\\.[^"\\]*)*)")' @@ -50,11 +53,6 @@ _blueprint_related = [ ] -def error(message): - print >> sys.stderr, 'error:', message - sys.exit(1) - - def make_diff(filename, old, new): for line in difflib.unified_diff(old.splitlines(), new.splitlines(), posixpath.normpath(posixpath.join('a', filename)), @@ -253,7 +251,7 @@ def walk_path(path): yield filename, 'python' else: with open(filename) as f: - contents = f.read() + contents = f.read(TEMPLATE_LOOKAHEAD) if '{% for' or '{% if' or '{{ url_for' in contents: yield filename, 'template' @@ -286,11 +284,6 @@ def autodetect_template_bundles(paths): def main(): """Entrypoint""" - if ast is None: - error('Python 2.6 or later is required to run the upgrade script.\n' - 'The runtime requirements for Flask 0.7 however are still ' - 'Python 2.5.') - parser = OptionParser() parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', action='store_true', help='Do not attempt to ' @@ -307,6 +300,11 @@ def main(): if not args: args = ['.'] + if ast is None: + parser.error('Python 2.6 or later is required to run the upgrade script.\n' + 'The runtime requirements for Flask 0.7 however are still ' + 'Python 2.5.') + if options.no_bundled_tmpl: template_bundles = False elif options.bundled_tmpl: From 9770054dee42df5e5a1f4dcf5c46e444ac86cb76 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:11:17 +0200 Subject: [PATCH 0616/3747] Started documenting the upgrade script --- docs/upgrading.rst | 34 +++++++++++++++++++++++++++++++++- scripts/flask-07-upgrade.py | 5 ++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 903524ca..e220bfc2 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -22,7 +22,39 @@ installation, make sure to pass it the ``-U`` parameter:: Version 0.7 ----------- -The following backwards incompatible changes exist from 0.6 to 0.7 +In Flask 0.7 we cleaned up the code base internally a lot and did some +backwards incompatible changes that make it easier to implement larger +applications with Flask. Because we want to make upgrading as easy as +possible we tried to counter the problems arising from these changes by +providing a script that can ease the transition. + +The script scans your whole application and generates an unified diff with +changes it assumes are safe to apply. However as this is an automated +tool it won't be able to find all use cases and it might miss some. We +internally spread a lot of deprecation warnings all over the place to make +it easy to find pieces of code that it was unable to upgrade. + +We strongly recommend that you hand review the generated patchfile and +only apply the chunks that look good. + +If you are using git as version control system for your project we +recommend applying the patch with ``path -p1 < patchfile.diff`` and then +using the interactive commit feature to only apply the chunks that look +good. + +To apply the upgrade script do the following: + +1. Download the script: `flask-07-upgrade.py + `_ +2. Run it in the directory of your application:: + + python flask-07-upgrade.py > patchfile.diff + +3. Review the generated patchfile. +4. Apply the patch:: + + patch -p1 < patchfile.diff + Bug in Request Locals ````````````````````` diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 216db1bf..d66e2dd8 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -245,11 +245,14 @@ def upgrade_template_file(filename, contents): def walk_path(path): for dirpath, dirnames, filenames in os.walk(path): + dirnames[:] = [x for x in dirnames if not x.startswith('.')] for filename in filenames: filename = os.path.join(dirpath, filename) if filename.endswith('.py'): yield filename, 'python' - else: + # skip files that are diffs. These might be false positives + # when run multiple times. + elif not filename.endswith(('.diff', '.patch', '.udiff')): with open(filename) as f: contents = f.read(TEMPLATE_LOOKAHEAD) if '{% for' or '{% if' or '{{ url_for' in contents: From 6094c8536bb9ac90abbca5c970bdc1bd645395ba Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:16:13 +0200 Subject: [PATCH 0617/3747] Usage for the script --- scripts/flask-07-upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index d66e2dd8..b3a3b1a2 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -287,7 +287,7 @@ def autodetect_template_bundles(paths): def main(): """Entrypoint""" - parser = OptionParser() + parser = OptionParser(usage='%prog [options] [paths]') parser.add_option('-T', '--no-teardown-detection', dest='no_teardown', action='store_true', help='Do not attempt to ' 'detect teardown function rewrites.') From 99bb902cc351c32c4646d6baeaa3de3bc9086718 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:21:31 +0200 Subject: [PATCH 0618/3747] Error handler deprecation --- scripts/flask-07-upgrade.py | 10 +++++++--- scripts/testproj/test.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index b3a3b1a2..daf915b6 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -35,21 +35,23 @@ except ImportError: TEMPLATE_LOOKAHEAD = 4096 - -_from_import_re = re.compile(r'^\s*from flask import\s+') +_app_re_part = r'((?:[a-zA-Z_][a-zA-Z0-9_]*app)|app|application)' _string_re_part = r"('([^'\\]*(?:\\.[^'\\]*)*)'" \ r'|"([^"\\]*(?:\\.[^"\\]*)*)")' + +_from_import_re = re.compile(r'^\s*from flask import\s+') _url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) _render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part) _after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') _module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' r'\(__name__\s*(?:,\s*(%s))?' % _string_re_part) +_error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part) _mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') _blueprint_related = [ (re.compile(r'request\.module'), 'request.blueprint'), (re.compile(r'register_module'), 'register_blueprint'), - (re.compile(r'(app|application)\.modules'), '\\1.blueprints') + (re.compile(r'%s\.modules' % _app_re_part), '\\1.blueprints') ] @@ -235,6 +237,8 @@ def upgrade_python_file(filename, contents, teardown, template_bundles): new_contents = fix_teardown_funcs(new_contents) new_contents = rewrite_for_blueprints(new_contents, filename, template_bundles) + new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]', + new_contents) make_diff(filename, contents, new_contents) diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py index fb999628..b892db1b 100644 --- a/scripts/testproj/test.py +++ b/scripts/testproj/test.py @@ -9,6 +9,11 @@ app.register_module(mod) app.register_module(mod2) +def handle_404(error): + return 'Testing', 404 +app.error_handlers[404] = handle_404 + + @app.after_request def after_request(response): g.db.close() From 60bd6209472bf47ddfc31e01b9150739219f6278 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:22:44 +0200 Subject: [PATCH 0619/3747] Warning about deprecation warnings not showing --- docs/upgrading.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index e220bfc2..59863143 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -55,6 +55,10 @@ To apply the upgrade script do the following: patch -p1 < patchfile.diff +Please note that deprecation warnings are disabled by default starting +with Python 2.7. In order to see the deprecation warnings that might be +emitted you have to enabled them with the :mod:`warnings` module. + Bug in Request Locals ````````````````````` From 207a7908e87e994f5ee53f9eb2cb83c9faedd9d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:26:18 +0200 Subject: [PATCH 0620/3747] Note for windows users on the upgrade script --- docs/upgrading.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 59863143..f0f3c26e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -59,6 +59,13 @@ Please note that deprecation warnings are disabled by default starting with Python 2.7. In order to see the deprecation warnings that might be emitted you have to enabled them with the :mod:`warnings` module. +If you are working with windows and you lack the `patch` command line +utility you can get it as part of various Unix runtime environments for +windows including cygwin, msysgit or ming32. Also source control systems +like svn, hg or git have builtin support for applying unified diffs as +generated by the tool. Check the manual of your version control system +for more information. + Bug in Request Locals ````````````````````` From c9cd6084c28a319985420cce401c810cf5236176 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 16:33:54 +0200 Subject: [PATCH 0621/3747] More upgrade notes for blueprints --- docs/upgrading.rst | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f0f3c26e..0a965d55 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -160,6 +160,43 @@ Alternatively you should just attach the function with a decorator:: (Note that :meth:`register_error_handler` is new in Flask 0.7) +Blueprint Support +````````````````` + +Blueprints replace the previous concept of “Modules” in Flask. They +provide better semantics for various features and work better with large +applications. The update script provided should be able to upgrade your +applications automatically, but there might be some cases where it fails +to upgrade. What changed? + +- Blueprints need explicit names. Modules had an automatic name + guesssing scheme where the shortname for the module was taken from the + last part of the import module. The upgrade script tries to guess + that name but it might fail as this information could change at + runtime. +- Blueprints have an inverse behavior for :meth:`url_for`. Previously + ``.foo`` told :meth:`url_for` that it should look for the endpoint + `foo` on the application. Now it means “relative to current module”. + The script will inverse all calls to :meth:`url_for` automatically for + you. It will do this in a very eager way so you might end up with + some unnecessary leading dots in your code if you're not using + modules. +- Blueprints do not automatically provide static folders. They will + still export templates from a folder called `templates` next to their + location however. If you want to continue serving static files you + need to tell the constructor explicitly the path to the static folder + (which can be relative to the blueprint's module path). +- Rendering templates was simplified. Now the general syntax is + ``blueprint-shortname:template-name`` for rendering templates instead + of ``blueprint-shortname/template-name`` which was confusing and often + clashed with templates from the global template loader. + +If you continue to use the `Module` object which is deprecated, Flask will +restore the previous behavior as good as possible. However we strongly +recommend upgrading to the new blueprints as they provide a lot of useful +improvement such as the ability to attach a blueprint multiple times, +blueprint specific error handlers and a lot more. + Version 0.6 ----------- From b4ffa4d9ac9cd652e94a97fa633f021d2f934582 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 6 Jun 2011 10:10:10 +0200 Subject: [PATCH 0622/3747] Allow name to be specified as the first parameter but with an explict name. --- scripts/flask-07-upgrade.py | 5 ++++- scripts/testproj/test.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index daf915b6..5df500c4 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -44,7 +44,7 @@ _url_for_re = re.compile(r'\b(url_for\()(%s)' % _string_re_part) _render_template_re = re.compile(r'\b(render_template\()(%s)' % _string_re_part) _after_request_re = re.compile(r'((?:@\S+\.(?:app_)?))(after_request)(\b\s*$)(?m)') _module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' - r'\(__name__\s*(?:,\s*(%s))?' % + r'\(__name__\s*(?:,\s*(?:name\s*=\s*)?(%s))?' % _string_re_part) _error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part) _mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') @@ -248,10 +248,13 @@ def upgrade_template_file(filename, contents): def walk_path(path): + this_file = os.path.realpath(__file__).rstrip('c') for dirpath, dirnames, filenames in os.walk(path): dirnames[:] = [x for x in dirnames if not x.startswith('.')] for filename in filenames: filename = os.path.join(dirpath, filename) + if os.path.realpath(filename) == this_file: + continue if filename.endswith('.py'): yield filename, 'python' # skip files that are diffs. These might be false positives diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py index b892db1b..8073f3c8 100644 --- a/scripts/testproj/test.py +++ b/scripts/testproj/test.py @@ -3,6 +3,7 @@ from flask import Flask, Module mod = Module(__name__) mod2 = Module(__name__, 'testmod2') +mod3 = Module(__name__, name='somemod', subdomain='meh') app = Flask(__name__) app.register_module(mod) From bfd67764fb48311e6a7ddcc85244726d70af677b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 7 Jun 2011 15:32:44 +0200 Subject: [PATCH 0623/3747] Started documentation for blueprints --- docs/blueprints.rst | 43 ++++++++++++++++++++++++++++++++ docs/contents.rst.inc | 1 + flask/blueprints.py | 53 +++++++++++++++++++++++++++------------- scripts/testproj/test.py | 27 ++++++++++---------- 4 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 docs/blueprints.rst diff --git a/docs/blueprints.rst b/docs/blueprints.rst new file mode 100644 index 00000000..016a2369 --- /dev/null +++ b/docs/blueprints.rst @@ -0,0 +1,43 @@ +.. _blueprints: + +Modular Applications with Blueprints +==================================== + +.. versionadded:: 0.7 + +Flask knows a concept known as “blueprints” which can greatly simplify how +large applications work. A blueprint is an object works similar to an +actual :class:`Flask` application object, but it is not actually an +application. Rather it is the blueprint of how to create an application. +Think of it like that: you might want to have an application that has a +wiki. So what you can do is creating the blueprint for a wiki and then +let the application assemble the wiki on the application object. + +Why Blueprints? +--------------- + +Why have blueprints and not multiple application objects? The utopia of +pluggable applications are different WSGI applications and merging them +together somehow. You can do that (see :ref:`app-dispatch`) but it's not +the right tool for every case. Having different applications means having +different configs. Applications are also separated on the WSGI layer +which is a lot lower level than the level that Flask usually operates on +where you have request and response objects. + +Blueprints do not necessarily have to implement applications. They could +only provide filters for templates, static files, templates or similar +things. They share the same config as the application and can change the +application as necessary when being registered. + +The downside is that you cannot unregister a blueprint once application +without having to destroy the whole application object. + +The Concept of Blueprints +------------------------- + +The basic concept of blueprints is that they record operations that should +be executed when the blueprint is registered on the application. However +additionally each time a request gets dispatched to a view that was +declared to a blueprint Flask will remember that the request was +dispatched to that blueprint. That way it's easier to generate URLs from +one endpoint to another in the same module. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 2689b4b5..6c49a878 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -18,6 +18,7 @@ instructions for web development with Flask. config signals reqcontext + blueprints shell patterns/index deploying/index diff --git a/flask/blueprints.py b/flask/blueprints.py index 1fed639d..41d5ef95 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -52,6 +52,9 @@ class Blueprint(_PackageBoundObject): .. versionadded:: 0.7 """ + warn_on_modifications = False + _got_registered_once = False + def __init__(self, name, import_name, static_folder=None, static_url_path=None, url_prefix=None, subdomain=None): @@ -64,14 +67,29 @@ class Blueprint(_PackageBoundObject): self.deferred_functions = [] self.view_functions = {} - def _record(self, func): + def record(self, func): + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + if self._got_registered_once and self.warn_on_modifications: + from warnings import warn + warn(Warning('The blueprint was already registered once ' + 'but is getting modified now. These changes ' + 'will not show up.')) self.deferred_functions.append(func) - def _record_once(self, func): + def record_once(self, func): + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ def wrapper(state): if state.first_registration: func(state) - return self._record(update_wrapper(wrapper, func)) + return self.record(update_wrapper(wrapper, func)) def make_setup_state(self, app, options, first_registration=False): return BlueprintSetupState(self, app, options, first_registration) @@ -83,6 +101,7 @@ class Blueprint(_PackageBoundObject): :func:`~flask.Flask.register_blueprint` are directly forwarded to this method in the `options` dictionary. """ + self._got_registered_once = True state = self.make_setup_state(app, options, first_registration) if self.has_static_folder: state.add_url_rule(self.static_url_path + '/', @@ -105,7 +124,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for the :func:`url_for` function is prefixed with the name of the module. """ - self._record(lambda s: + self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) def endpoint(self, endpoint): @@ -118,7 +137,7 @@ class Blueprint(_PackageBoundObject): def decorator(f): def register_endpoint(state): state.app.view_functions[endpoint] = f - self._record_once(register_endpoint) + self.record_once(register_endpoint) return f return decorator @@ -127,7 +146,7 @@ class Blueprint(_PackageBoundObject): is only executed before each request that is handled by a function of that module. """ - self._record_once(lambda s: s.app.before_request_funcs + self.record_once(lambda s: s.app.before_request_funcs .setdefault(self.name, []).append(f)) return f @@ -135,7 +154,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.before_request`. Such a function is executed before each request, even if outside of a module. """ - self._record_once(lambda s: s.app.before_request_funcs + self.record_once(lambda s: s.app.before_request_funcs .setdefault(None, []).append(f)) return f @@ -144,7 +163,7 @@ class Blueprint(_PackageBoundObject): is only executed after each request that is handled by a function of that module. """ - self._record_once(lambda s: s.app.after_request_funcs + self.record_once(lambda s: s.app.after_request_funcs .setdefault(self.name, []).append(f)) return f @@ -152,7 +171,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.after_request` but for a module. Such a function is executed after each request, even if outside of the module. """ - self._record_once(lambda s: s.app.after_request_funcs + self.record_once(lambda s: s.app.after_request_funcs .setdefault(None, []).append(f)) return f @@ -160,7 +179,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.context_processor` but for a module. This function is only executed for requests handled by a module. """ - self._record_once(lambda s: s.app.template_context_processors + self.record_once(lambda s: s.app.template_context_processors .setdefault(self.name, []).append(f)) return f @@ -168,7 +187,7 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.context_processor` but for a module. Such a function is executed each request, even if outside of the module. """ - self._record_once(lambda s: s.app.template_context_processors + self.record_once(lambda s: s.app.template_context_processors .setdefault(None, []).append(f)) return f @@ -177,7 +196,7 @@ class Blueprint(_PackageBoundObject): handler is used for all requests, even if outside of the module. """ def decorator(f): - self._record_once(lambda s: s.app.errorhandler(code)(f)) + self.record_once(lambda s: s.app.errorhandler(code)(f)) return f return decorator @@ -186,7 +205,7 @@ class Blueprint(_PackageBoundObject): blueprint. It's called before the view functions are called and can modify the url values provided. """ - self._record_once(lambda s: s.app.url_value_preprocessors + self.record_once(lambda s: s.app.url_value_preprocessors .setdefault(self.name, []).append(f)) return f @@ -195,21 +214,21 @@ class Blueprint(_PackageBoundObject): with the endpoint and values and should update the values passed in place. """ - self._record_once(lambda s: s.app.url_default_functions + self.record_once(lambda s: s.app.url_default_functions .setdefault(self.name, []).append(f)) return f def app_url_value_preprocessor(self, f): """Same as :meth:`url_value_preprocessor` but application wide. """ - self._record_once(lambda s: s.app.url_value_preprocessor + self.record_once(lambda s: s.app.url_value_preprocessor .setdefault(self.name, []).append(f)) return f def app_url_defaults(self, f): """Same as :meth:`url_defaults` but application wide. """ - self._record_once(lambda s: s.app.url_default_functions + self.record_once(lambda s: s.app.url_default_functions .setdefault(None, []).append(f)) return f @@ -227,7 +246,7 @@ class Blueprint(_PackageBoundObject): .. versionadded:: 0.7 """ def decorator(f): - self._record_once(lambda s: s.app._register_error_handler( + self.record_once(lambda s: s.app._register_error_handler( self.name, code_or_exception, f)) return f return decorator diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py index 8073f3c8..e7b68eaf 100644 --- a/scripts/testproj/test.py +++ b/scripts/testproj/test.py @@ -1,19 +1,10 @@ -from flask import Flask, Module +from flask import Flask, Module, render_template mod = Module(__name__) mod2 = Module(__name__, 'testmod2') mod3 = Module(__name__, name='somemod', subdomain='meh') -app = Flask(__name__) -app.register_module(mod) -app.register_module(mod2) - - -def handle_404(error): - return 'Testing', 404 -app.error_handlers[404] = handle_404 - @app.after_request def after_request(response): @@ -27,10 +18,20 @@ def index(): @mod2.route('/') -def index(): +def mod2_index(): return render_template('testmod2/index.html') -@mod2.route('/') -def index(): +@mod3.route('/') +def mod3_index(): return render_template('something-else/index.html') + + +app = Flask(__name__) +app.register_module(mod) +app.register_module(mod2) + + +def handle_404(error): + return 'Testing', 404 +app.error_handlers[404] = handle_404 From 311c244f2e9fdfa0a39cbf1b8372cb6a5fe6fb90 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 7 Jun 2011 20:45:26 -0400 Subject: [PATCH 0624/3747] Approve Flask-Login. --- extreview/approved.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/extreview/approved.rst b/extreview/approved.rst index 3a21d496..82bbf9f7 100644 --- a/extreview/approved.rst +++ b/extreview/approved.rst @@ -155,3 +155,14 @@ Frozen-Flask :Approved License: BSD All fine. Posted recommendations for minor items/enhancements to mailing list. + + +Flask-Login +----------- + +:First Approval: 2011-06-07 +:Last Review: 2011-06-07 +:Approved Version: 0.1 +:Approved License: MIT + +All fine. From 2647bec408b676e5639d3d20ce4c1f9a53188463 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 8 Jun 2011 16:22:17 +0200 Subject: [PATCH 0625/3747] Added a minimal blueprint example --- docs/blueprints.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 016a2369..9d429df1 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -41,3 +41,22 @@ additionally each time a request gets dispatched to a view that was declared to a blueprint Flask will remember that the request was dispatched to that blueprint. That way it's easier to generate URLs from one endpoint to another in the same module. + +My First Blueprint +------------------ + +This is what a very basic blueprint looks like. In this case we want to +implement a blueprint that does simple rendering of static templates:: + + from flask import Blueprint, render_template, abort + from jinja2 import TemplateNotFound + + simple_page = Blueprint('simple_page', __name__) + + @simple_page.route('/', defaults={'page': 'index'}) + @simple_page.route('/') + def show(page): + try: + return render_template('simple_pages/%s.html' % page) + except TemplateNotFound: + abort(404) From 20bf8edcf4efed9405c6148ee070c22c32fbb7ea Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 7 Jun 2011 21:55:07 -0400 Subject: [PATCH 0626/3747] Rename teardown example to avoid confusion. --- docs/reqcontext.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 088502eb..832f0e66 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -174,13 +174,14 @@ It's easy to see the behavior from the command line: >>> app = Flask(__name__) >>> @app.teardown_request -... def after_request(exception=None): -... print 'after request' -... +... def teardown_request(exception=None): +... print 'this runs after request' +... >>> ctx = app.test_request_context() >>> ctx.push() >>> ctx.pop() -after request +this runs after request +>>> .. _notes-on-proxies: From 0313c8b24a40d016e6b64495ce72ed912ce0fc24 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 8 Jun 2011 08:20:42 -0400 Subject: [PATCH 0627/3747] Expand documentation on blueprints. --- docs/blueprints.rst | 61 ++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 9d429df1..04ab0d8b 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -5,42 +5,51 @@ Modular Applications with Blueprints .. versionadded:: 0.7 -Flask knows a concept known as “blueprints” which can greatly simplify how -large applications work. A blueprint is an object works similar to an -actual :class:`Flask` application object, but it is not actually an -application. Rather it is the blueprint of how to create an application. -Think of it like that: you might want to have an application that has a -wiki. So what you can do is creating the blueprint for a wiki and then -let the application assemble the wiki on the application object. +Flask uses a concept of *blueprints* for making application components and +supporting common patterns within an application or across applications. +Blueprints can greatly simplify how large applications work and provide a +central means for Flask extensions to register operations on applications. +A :class:`Blueprint` object works similarly to a :class:`Flask` +application object, but it is not actually an application. Rather it is a +*blueprint* of how to construct or extend an application. Why Blueprints? --------------- -Why have blueprints and not multiple application objects? The utopia of -pluggable applications are different WSGI applications and merging them -together somehow. You can do that (see :ref:`app-dispatch`) but it's not -the right tool for every case. Having different applications means having -different configs. Applications are also separated on the WSGI layer -which is a lot lower level than the level that Flask usually operates on -where you have request and response objects. +Blueprints in Flask are intended for these cases: -Blueprints do not necessarily have to implement applications. They could -only provide filters for templates, static files, templates or similar -things. They share the same config as the application and can change the -application as necessary when being registered. +* Factor an application into a set of blueprints. This is ideal for + larger applications; a project could instantiate an application object, + initialize several extensions, and register a collection of blueprints. +* Register a blueprint on an application at a URL prefix and/or subdomain. + Paremeters in the URL prefix/subdomain become common view arguments + (with defaults) across all view functions in the blueprint. +* Register a blueprint multiple times on an application with different URL + rules. +* Provide template filters, static files, templates, and other utilities + through blueprints. A blueprint does not have to implement applications + or view functions. +* Register a blueprint on an application for any of these cases when + initializing a Flask extension. -The downside is that you cannot unregister a blueprint once application -without having to destroy the whole application object. +A blueprint in Flask is not a pluggable app because it is not actually an +application -- it's a set of operations which can be registered on an +application, even multiple times. Why not have multiple application +objects? You can do that (see :ref:`app-dispatch`), but your applications +will have separate configs and will be managed at the WSGI layer. + +Blueprints instead provide separation at the Flask level, share +application config, and can change an application object as necessary with +being registered. The downside is that you cannot unregister a blueprint +once application without having to destroy the whole application object. The Concept of Blueprints ------------------------- -The basic concept of blueprints is that they record operations that should -be executed when the blueprint is registered on the application. However -additionally each time a request gets dispatched to a view that was -declared to a blueprint Flask will remember that the request was -dispatched to that blueprint. That way it's easier to generate URLs from -one endpoint to another in the same module. +The basic concept of blueprints is that they record operations to execute +when registered on an application. Flask associates view functions with +blueprints when dispatching requests and generating URLs from one endpoint +to another. My First Blueprint ------------------ From 3d6e3e48ad2445c80761c930ddfbe0ce24be3eb3 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 8 Jun 2011 10:59:32 -0400 Subject: [PATCH 0628/3747] Replace 'module' with 'blueprint' in docstrings. --- flask/blueprints.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index 41d5ef95..03f01748 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -112,8 +112,8 @@ class Blueprint(_PackageBoundObject): deferred(state) def route(self, rule, **options): - """Like :meth:`Flask.route` but for a module. The endpoint for the - :func:`url_for` function is prefixed with the name of the module. + """Like :meth:`Flask.route` but for a blueprint. The endpoint for the + :func:`url_for` function is prefixed with the name of the blueprint. """ def decorator(f): self.add_url_rule(rule, f.__name__, f, **options) @@ -121,15 +121,15 @@ class Blueprint(_PackageBoundObject): return decorator def add_url_rule(self, rule, endpoint=None, view_func=None, **options): - """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for - the :func:`url_for` function is prefixed with the name of the module. + """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for + the :func:`url_for` function is prefixed with the name of the blueprint. """ self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) def endpoint(self, endpoint): - """Like :meth:`Flask.endpoint` but for a module. This does not - prefix the endpoint with the module name, this has to be done + """Like :meth:`Flask.endpoint` but for a blueprint. This does not + prefix the endpoint with the blueprint name, this has to be done explicitly by the user of this method. If the endpoint is prefixed with a `.` it will be registered to the current blueprint, otherwise it's an application independent endpoint. @@ -142,9 +142,9 @@ class Blueprint(_PackageBoundObject): return decorator def before_request(self, f): - """Like :meth:`Flask.before_request` but for a module. This function + """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of - that module. + that blueprint. """ self.record_once(lambda s: s.app.before_request_funcs .setdefault(self.name, []).append(f)) @@ -152,48 +152,48 @@ class Blueprint(_PackageBoundObject): def before_app_request(self, f): """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a module. + before each request, even if outside of a blueprint. """ self.record_once(lambda s: s.app.before_request_funcs .setdefault(None, []).append(f)) return f def after_request(self, f): - """Like :meth:`Flask.after_request` but for a module. This function + """Like :meth:`Flask.after_request` but for a blueprint. This function is only executed after each request that is handled by a function of - that module. + that blueprint. """ self.record_once(lambda s: s.app.after_request_funcs .setdefault(self.name, []).append(f)) return f def after_app_request(self, f): - """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request, even if outside of the module. + """Like :meth:`Flask.after_request` but for a blueprint. Such a function + is executed after each request, even if outside of the blueprint. """ self.record_once(lambda s: s.app.after_request_funcs .setdefault(None, []).append(f)) return f def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. This - function is only executed for requests handled by a module. + """Like :meth:`Flask.context_processor` but for a blueprint. This + function is only executed for requests handled by a blueprint. """ self.record_once(lambda s: s.app.template_context_processors .setdefault(self.name, []).append(f)) return f def app_context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. Such a - function is executed each request, even if outside of the module. + """Like :meth:`Flask.context_processor` but for a blueprint. Such a + function is executed each request, even if outside of the blueprint. """ self.record_once(lambda s: s.app.template_context_processors .setdefault(None, []).append(f)) return f def app_errorhandler(self, code): - """Like :meth:`Flask.errorhandler` but for a module. This - handler is used for all requests, even if outside of the module. + """Like :meth:`Flask.errorhandler` but for a blueprint. This + handler is used for all requests, even if outside of the blueprint. """ def decorator(f): self.record_once(lambda s: s.app.errorhandler(code)(f)) @@ -210,7 +210,7 @@ class Blueprint(_PackageBoundObject): return f def url_defaults(self, f): - """Callback function for URL defaults for this module. It's called + """Callback function for URL defaults for this blueprint. It's called with the endpoint and values and should update the values passed in place. """ From 5127b8bd26a58cb9ac8214a89cee16eb14349e59 Mon Sep 17 00:00:00 2001 From: Merlin Date: Fri, 10 Jun 2011 01:38:26 -0700 Subject: [PATCH 0629/3747] Added initial uwsgi deployment documentation for nginx; updated nginx URL in fastcgi deployment docs; added uwsgi to deployment index --- docs/deploying/fastcgi.rst | 2 +- docs/deploying/index.rst | 1 + docs/deploying/uwsgi.rst | 63 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 docs/deploying/uwsgi.rst diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index eeaee43e..31d93fb9 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -148,5 +148,5 @@ path. Common problems are: - different python interpreters being used. .. _lighttpd: http://www.lighttpd.net/ -.. _nginx: http://nginx.net/ +.. _nginx: http://nginx.org/ .. _flup: http://trac.saddi.com/flup diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index a59e4e9a..64c1a239 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -16,4 +16,5 @@ is the actual WSGI application. mod_wsgi cgi fastcgi + uwsgi others diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst new file mode 100644 index 00000000..7ec153fd --- /dev/null +++ b/docs/deploying/uwsgi.rst @@ -0,0 +1,63 @@ +uWSGI +===== + +A newly popular deployment method on servers like `cherokee`_ and `nginx`_ +is uWSGI. To use your WSGI application with uWSGI protocol you will need +a uWSGI server first. uWSGI is both a protocol and an application server; +the application server can serve uWSGI, FastCGI, and HTTP protocols. + +The most popular server is `uwsgi`_, which we will use for this guide. +Make sure to have it installed. + +.. admonition:: Watch Out + + Please make sure in advance that your ``app.run()`` call you might + have in your application file, is inside an ``if __name__ == + '__main__':`` or moved to a separate file. Just make sure it's not + called because this will always start a local WSGI server which we do + not want if we deploy that application to uWSGI. + +Starting your app with uwsgi +---------------------------- + +`uwsgi` is designed to operate on WSGI callables found in python modules. + +Given a flask application in myapp.py, use the following command: + +.. sourcecode:: text + + $ uwsgi -s /tmp/uwsgi.sock --module myapp --callable app + +Or, if you prefer: + +.. sourcecode:: text + + $ uwsgi -s /tmp/uwsgi.sock myapp:app + +Configuring nginx +----------------- + +A basic flask uWSGI configuration for nginx looks like this:: + + location = /yourapplication { rewrite ^ /yourapplication/; } + location /yourapplication { try_files $uri @yourapplication; } + location @yourapplication { + include uwsgi_params; + uwsgi_param SCRIPT_NAME /yourapplication; + uwsgi_modifier1 30; + uwsgi_pass unix:/tmp/uwsgi.sock; + } + +This configuration binds the application to `/yourapplication`. If you want +to have it in the URL root it's a bit simpler because you don't have to tell +it the WSGI `SCRIPT_NAME` or set the uwsgi modifer to make use of it:: + + location / { try_files $uri @yourapplication; } + location @yourapplication { + include uwsgi_params; + uwsgi_pass unix:/tmp/uwsgi.sock; + } + +.. _cherokee: http://www.cherokee-project.com/ +.. _nginx: http://nginx.org/ +.. _uwsgi: http://projects.unbit.it/uwsgi/ From 207006f4c3ad0dc74a1a9c84b4d1dfad50e58407 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 10 Jun 2011 12:15:50 -0400 Subject: [PATCH 0630/3747] Touch up and integrate docs on deploying Flask. --- docs/deploying/cgi.rst | 23 +++++---- docs/deploying/fastcgi.rst | 96 +++++++++++++++++++++---------------- docs/deploying/index.rst | 15 +++--- docs/deploying/mod_wsgi.rst | 69 +++++++++++++------------- docs/deploying/others.rst | 34 +++++++------ docs/deploying/uwsgi.rst | 31 +++++++----- 6 files changed, 145 insertions(+), 123 deletions(-) diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 830ab28a..a2fba90d 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -1,23 +1,20 @@ CGI === -If all other deployment methods do not work, CGI will work for sure. CGI -is supported by all major servers but usually has a less-than-optimal +If all other deployment methods do not work, CGI will work for sure. +CGI is supported by all major servers but usually has a sub-optimal performance. -This is also the way you can use a Flask application on Google's -`App Engine`_, there however the execution does happen in a CGI-like -environment. The application's performance is unaffected because of that. +This is also the way you can use a Flask application on Google's `App +Engine`_, where execution happens in a CGI-like environment. .. admonition:: Watch Out - Please make sure in advance that your ``app.run()`` call you might - have in your application file, is inside an ``if __name__ == - '__main__':`` or moved to a separate file. Just make sure it's not - called because this will always start a local WSGI server which we do - not want if we deploy that application to CGI / app engine. - -.. _App Engine: http://code.google.com/appengine/ + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to CGI / app engine. Creating a `.cgi` file ---------------------- @@ -45,3 +42,5 @@ In Apache for example you can put a like like this into the config: ScriptAlias /app /path/to/the/application.cgi For more information consult the documentation of your webserver. + +.. _App Engine: http://code.google.com/appengine/ diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 31d93fb9..0b5d887c 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -1,20 +1,22 @@ +.. _deploying-fastcgi: + FastCGI ======= -A very popular deployment setup on servers like `lighttpd`_ and `nginx`_ -is FastCGI. To use your WSGI application with any of them you will need -a FastCGI server first. - -The most popular one is `flup`_ which we will use for this guide. Make -sure to have it installed. +FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, +and `cherokee`_; see :ref:`deploying-uwsgi` and +:ref:`deploying-other-servers` for other options. To use your WSGI +application with any of them you will need a FastCGI server first. The +most popular one is `flup`_ which we will use for this guide. Make sure +to have it installed to follow along. .. admonition:: Watch Out - Please make sure in advance that your ``app.run()`` call you might - have in your application file, is inside an ``if __name__ == - '__main__':`` or moved to a separate file. Just make sure it's not - called because this will always start a local WSGI server which we do - not want if we deploy that application to FastCGI. + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to FastCGI. Creating a `.fcgi` file ----------------------- @@ -25,14 +27,14 @@ First you need to create the FastCGI server file. Let's call it #!/usr/bin/python from flup.server.fcgi import WSGIServer from yourapplication import app - + if __name__ == '__main__': WSGIServer(app).run() This is enough for Apache to work, however nginx and older versions of -lighttpd need a socket to be explicitly passed to communicate with the FastCGI -server. For that to work you need to pass the path to the socket to the -:class:`~flup.server.fcgi.WSGIServer`:: +lighttpd need a socket to be explicitly passed to communicate with the +FastCGI server. For that to work you need to pass the path to the +socket to the :class:`~flup.server.fcgi.WSGIServer`:: WSGIServer(application, bindAddress='/path/to/fcgi.sock').run() @@ -72,22 +74,23 @@ A basic FastCGI configuration for lighttpd looks like that:: "^(/static.*)$" => "$1", "^(/.*)$" => "/yourapplication.fcgi$1" -Remember to enable the FastCGI, alias and rewrite modules. This configuration -binds the application to `/yourapplication`. If you want the application to -work in the URL root you have to work around a lighttpd bug with the +Remember to enable the FastCGI, alias and rewrite modules. This +configuration binds the application to `/yourapplication`. If you want +the application to work in the URL root you have to work around a +lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. Make sure to apply it only if you are mounting the application the URL -root. Also, see the Lighty docs for more information on `FastCGI and Python -`_ (note that -explicitly passing a socket to run() is no longer necessary). +root. Also, see the Lighty docs for more information on `FastCGI and +Python `_ +(note that explicitly passing a socket to run() is no longer necessary). Configuring nginx ----------------- -Installing FastCGI applications on nginx is a bit different because by default -no FastCGI parameters are forwarded. +Installing FastCGI applications on nginx is a bit different because by +default no FastCGI parameters are forwarded. A basic flask FastCGI configuration for nginx looks like this:: @@ -101,9 +104,9 @@ A basic flask FastCGI configuration for nginx looks like this:: fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; } -This configuration binds the application to `/yourapplication`. If you want -to have it in the URL root it's a bit simpler because you don't have to figure -out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: +This configuration binds the application to `/yourapplication`. If you +want to have it in the URL root it's a bit simpler because you don't +have to figure out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: location / { try_files $uri @yourapplication; } location @yourapplication { @@ -113,9 +116,17 @@ out how to calculate `PATH_INFO` and `SCRIPT_NAME`:: fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; } -Since Nginx doesn't load FastCGI apps, you have to do it by yourself. You -can either write an `init.d` script for that or execute it inside a screen -session:: +Running FastCGI Processes +------------------------- + +Since Nginx and others do not load FastCGI apps, you have to do it by +yourself. `Supervisor can manage FastCGI processes. +`_ +You can look around for other FastCGI process managers or write a script +to run your `.fcgi` file at boot, e.g. using a SysV ``init.d`` script. +For a temporary solution, you can always run the ``.fcgi`` script inside +GNU screen. See ``man screen`` for details, and note that this is a +manual solution which does not persist across system restart:: $ screen $ /var/www/yourapplication/yourapplication.fcgi @@ -123,14 +134,14 @@ session:: Debugging --------- -FastCGI deployments tend to be hard to debug on most webservers. Very often the -only thing the server log tells you is something along the lines of "premature -end of headers". In order to debug the application the only thing that can -really give you ideas why it breaks is switching to the correct user and -executing the application by hand. +FastCGI deployments tend to be hard to debug on most webservers. Very +often the only thing the server log tells you is something along the +lines of "premature end of headers". In order to debug the application +the only thing that can really give you ideas why it breaks is switching +to the correct user and executing the application by hand. -This example assumes your application is called `application.fcgi` and that your -webserver user is `www-data`:: +This example assumes your application is called `application.fcgi` and +that your webserver user is `www-data`:: $ su www-data $ cd /var/www/yourapplication @@ -139,14 +150,15 @@ webserver user is `www-data`:: File "yourapplication.fcgi", line 4, in ImportError: No module named yourapplication -In this case the error seems to be "yourapplication" not being on the python -path. Common problems are: +In this case the error seems to be "yourapplication" not being on the +python path. Common problems are: -- relative paths being used. Don't rely on the current working directory -- the code depending on environment variables that are not set by the +- Relative paths being used. Don't rely on the current working directory +- The code depending on environment variables that are not set by the web server. -- different python interpreters being used. +- Different python interpreters being used. -.. _lighttpd: http://www.lighttpd.net/ .. _nginx: http://nginx.org/ +.. _lighttpd: http://www.lighttpd.net/ +.. _cherokee: http://www.cherokee-project.com/ .. _flup: http://trac.saddi.com/flup diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 64c1a239..bd0d1626 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -1,14 +1,15 @@ Deployment Options ================== -Depending on what you have available there are multiple ways to run Flask -applications. A very common method is to use the builtin server during -development and maybe behind a proxy for simple applications, but there -are more options available. +Depending on what you have available there are multiple ways to run +Flask applications. You can use the builtin server during development, +but you should use a full deployment option for production applications. +(Do not use the builtin development server in production.) Several +options are available and documented here. -If you have a different WSGI server look up the server documentation about -how to use a WSGI app with it. Just remember that your application object -is the actual WSGI application. +If you have a different WSGI server look up the server documentation +about how to use a WSGI app with it. Just remember that your +:class:`Flask` application object is the actual WSGI application. .. toctree:: :maxdepth: 2 diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 40df522d..c85ed64f 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -3,35 +3,34 @@ mod_wsgi (Apache) ================= -If you are using the `Apache`_ webserver you should consider using `mod_wsgi`_. +If you are using the `Apache`_ webserver, consider using `mod_wsgi`_. .. admonition:: Watch Out - Please make sure in advance that your ``app.run()`` call you might - have in your application file, is inside an ``if __name__ == - '__main__':`` or moved to a separate file. Just make sure it's not - called because this will always start a local WSGI server which we do - not want if we deploy that application to mod_wsgi. + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to mod_wsgi. .. _Apache: http://httpd.apache.org/ Installing `mod_wsgi` --------------------- -If you don't have `mod_wsgi` installed yet you have to either install it using -a package manager or compile it yourself. +If you don't have `mod_wsgi` installed yet you have to either install it +using a package manager or compile it yourself. The mod_wsgi +`installation instructions`_ cover source installations on UNIX systems. -The mod_wsgi `installation instructions`_ cover source installations on UNIX -systems. - -If you are using Ubuntu/Debian you can apt-get it and activate it as follows: +If you are using Ubuntu/Debian you can apt-get it and activate it as +follows: .. sourcecode:: text # apt-get install libapache2-mod-wsgi -On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by using -pkg_add: +On FreeBSD install `mod_wsgi` by compiling the `www/mod_wsgi` port or by +using pkg_add: .. sourcecode:: text @@ -40,8 +39,8 @@ pkg_add: If you are using pkgsrc you can install `mod_wsgi` by compiling the `www/ap2-wsgi` package. -If you encounter segfaulting child processes after the first apache reload you -can safely ignore them. Just restart the server. +If you encounter segfaulting child processes after the first apache +reload you can safely ignore them. Just restart the server. Creating a `.wsgi` file ----------------------- @@ -61,14 +60,15 @@ instance you can directly import that one as `application`. Store that file somewhere that you will find it again (e.g.: `/var/www/yourapplication`) and make sure that `yourapplication` and all the libraries that are in use are on the python load path. If you don't -want to install it system wide consider using a `virtual python`_ instance. +want to install it system wide consider using a `virtual python`_ +instance. Configuring Apache ------------------ -The last thing you have to do is to create an Apache configuration file for -your application. In this example we are telling `mod_wsgi` to execute the -application under a different user for security reasons: +The last thing you have to do is to create an Apache configuration file +for your application. In this example we are telling `mod_wsgi` to +execute the application under a different user for security reasons: .. sourcecode:: apache @@ -100,15 +100,16 @@ If your application does not run, follow this guide to troubleshoot: **Problem:** application does not run, errorlog shows SystemExit ignored You have a ``app.run()`` call in your application file that is not - guarded by an ``if __name__ == '__main__':`` condition. Either remove - that :meth:`~flask.Flask.run` call from the file and move it into a - separate `run.py` file or put it into such an if block. + guarded by an ``if __name__ == '__main__':`` condition. Either + remove that :meth:`~flask.Flask.run` call from the file and move it + into a separate `run.py` file or put it into such an if block. **Problem:** application gives permission errors Probably caused by your application running as the wrong user. Make sure the folders the application needs access to have the proper - privileges set and the application runs as the correct user (``user`` - and ``group`` parameter to the `WSGIDaemonProcess` directive) + privileges set and the application runs as the correct user + (``user`` and ``group`` parameter to the `WSGIDaemonProcess` + directive) **Problem:** application dies with an error on print Keep in mind that mod_wsgi disallows doing anything with @@ -127,10 +128,10 @@ If your application does not run, follow this guide to troubleshoot: sys.stdout = sys.stderr **Problem:** accessing resources gives IO errors - Your application probably is a single .py file you symlinked into the - site-packages folder. Please be aware that this does not work, - instead you either have to put the folder into the pythonpath the file - is stored in, or convert your application into a package. + Your application probably is a single .py file you symlinked into + the site-packages folder. Please be aware that this does not work, + instead you either have to put the folder into the pythonpath the + file is stored in, or convert your application into a package. The reason for this is that for non-installed packages, the module filename is used to locate the resources and for symlinks the wrong @@ -139,9 +140,9 @@ If your application does not run, follow this guide to troubleshoot: Support for Automatic Reloading ------------------------------- -To help deployment tools you can activate support for automatic reloading. -Whenever something changes the `.wsgi` file, `mod_wsgi` will reload all -the daemon processes for us. +To help deployment tools you can activate support for automatic +reloading. Whenever something changes the `.wsgi` file, `mod_wsgi` will +reload all the daemon processes for us. For that, just add the following directive to your `Directory` section: @@ -154,8 +155,8 @@ Working with Virtual Environments Virtual environments have the advantage that they never install the required dependencies system wide so you have a better control over what -is used where. If you want to use a virtual environment with mod_wsgi you -have to modify your `.wsgi` file slightly. +is used where. If you want to use a virtual environment with mod_wsgi +you have to modify your `.wsgi` file slightly. Add the following lines to the top of your `.wsgi` file:: diff --git a/docs/deploying/others.rst b/docs/deploying/others.rst index 153fb7cf..6f3e5cc6 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/others.rst @@ -1,11 +1,11 @@ +.. _deploying-other-servers: + Other Servers ============= -There are popular servers written in Python that allow the execution of -WSGI applications as well. Keep in mind though that some of these servers -were written for very specific applications and might not work as well for -standard WSGI application such as Flask powered ones. - +There are popular servers written in Python that allow the execution of WSGI +applications as well. These servers stand alone when they run; you can proxy +to them from your web server. Tornado -------- @@ -15,12 +15,12 @@ server and tools that power `FriendFeed`_. Because it is non-blocking and uses epoll, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. Integrating this service with Flask is a trivial task:: - + from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop from yourapplication import app - + http_server = HTTPServer(WSGIContainer(app)) http_server.listen(5000) IOLoop.instance().start() @@ -29,7 +29,6 @@ service with Flask is a trivial task:: .. _Tornado: http://www.tornadoweb.org/ .. _FriendFeed: http://friendfeed.com/ - Gevent ------- @@ -47,7 +46,6 @@ event loop:: .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ - Gunicorn -------- @@ -57,19 +55,25 @@ and `greenlet`_. Running a Flask application on this server is quite simple:: gunicorn myproject:app +`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. +For example, to run a Flask application with 4 worker processes (``-w +4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: + + gunicorn -w 4 -b 127.0.0.1:4000 myproject:app + .. _Gunicorn: http://gunicorn.org/ .. _eventlet: http://eventlet.net/ .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html - Proxy Setups ------------ -If you deploy your application behind an HTTP proxy you will need to -rewrite a few headers in order for the application to work. The two -problematic values in the WSGI environment usually are `REMOTE_ADDR` and -`HTTP_HOST`. Werkzeug ships a fixer that will solve some common setups, -but you might want to write your own WSGI middleware for specific setups. +If you deploy your application using one of these servers behind an HTTP +proxy you will need to rewrite a few headers in order for the +application to work. The two problematic values in the WSGI environment +usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that +will solve some common setups, but you might want to write your own WSGI +middleware for specific setups. The most common setup invokes the host being set from `X-Forwarded-Host` and the remote address from `X-Forwarded-For`:: diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index 7ec153fd..6f373731 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -1,21 +1,25 @@ +.. _deploying-uwsgi: + uWSGI ===== -A newly popular deployment method on servers like `cherokee`_ and `nginx`_ -is uWSGI. To use your WSGI application with uWSGI protocol you will need -a uWSGI server first. uWSGI is both a protocol and an application server; -the application server can serve uWSGI, FastCGI, and HTTP protocols. +uWSGI is a deployment option on servers like `nginx`_, `lighttpd`_, and +`cherokee`_; see :ref:`deploying-fastcgi` and +:ref:`deploying-other-servers` for other options. To use your WSGI +application with uWSGI protocol you will need a uWSGI server +first. uWSGI is both a protocol and an application server; the +application server can serve uWSGI, FastCGI, and HTTP protocols. -The most popular server is `uwsgi`_, which we will use for this guide. -Make sure to have it installed. +The most popular uWSGI server is `uwsgi`_, which we will use for this +guide. Make sure to have it installed to follow along. .. admonition:: Watch Out - Please make sure in advance that your ``app.run()`` call you might - have in your application file, is inside an ``if __name__ == - '__main__':`` or moved to a separate file. Just make sure it's not - called because this will always start a local WSGI server which we do - not want if we deploy that application to uWSGI. + Please make sure in advance that any ``app.run()`` calls you might + have in your application file are inside an ``if __name__ == + '__main__':`` block or moved to a separate file. Just make sure it's + not called because this will always start a local WSGI server which + we do not want if we deploy that application to uWSGI. Starting your app with uwsgi ---------------------------- @@ -50,7 +54,7 @@ A basic flask uWSGI configuration for nginx looks like this:: This configuration binds the application to `/yourapplication`. If you want to have it in the URL root it's a bit simpler because you don't have to tell -it the WSGI `SCRIPT_NAME` or set the uwsgi modifer to make use of it:: +it the WSGI `SCRIPT_NAME` or set the uwsgi modifier to make use of it:: location / { try_files $uri @yourapplication; } location @yourapplication { @@ -58,6 +62,7 @@ it the WSGI `SCRIPT_NAME` or set the uwsgi modifer to make use of it:: uwsgi_pass unix:/tmp/uwsgi.sock; } -.. _cherokee: http://www.cherokee-project.com/ .. _nginx: http://nginx.org/ +.. _lighttpd: http://www.lighttpd.net/ +.. _cherokee: http://www.cherokee-project.com/ .. _uwsgi: http://projects.unbit.it/uwsgi/ From 87234fb2f8c896bd2df336bdce5ffb3e85112cdc Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 15 Jun 2011 23:31:48 -0700 Subject: [PATCH 0631/3747] fixed typo --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fc5ae0f1..179ff2de 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -11,7 +11,7 @@ you do not, head over to the :ref:`installation` section. A Minimal Application --------------------- -A minimal Flask application looks something like that:: +A minimal Flask application looks something like this:: from flask import Flask app = Flask(__name__) From 65f9bc7b226ed8e65fc48bb26f6f68c2e2844720 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 16 Jun 2011 10:28:51 -0400 Subject: [PATCH 0632/3747] Touch up (tiny) doc as requested by emiel, #244. --- docs/patterns/appfactories.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 134ffcf4..8222bac5 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -39,7 +39,7 @@ The idea is to set up the application in a function. Like this:: The downside is that you cannot use the application object in the modules at import time. You can however use it from within a request. How do you -get access the application with the config? Use +get access to the application with the config? Use :data:`~flask.current_app`:: from flask import current_app, Module, render_template From 59a92ebd71121afc55f4a2f3bd305ca0a2222169 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 16 Jun 2011 23:52:50 +0200 Subject: [PATCH 0633/3747] Updated blueprint documentation --- docs/blueprints.rst | 90 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 9d429df1..8a83128d 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -57,6 +57,94 @@ implement a blueprint that does simple rendering of static templates:: @simple_page.route('/') def show(page): try: - return render_template('simple_pages/%s.html' % page) + return render_template('pages/%s.html' % page) except TemplateNotFound: abort(404) + +When you bind a function with the help of the ``@simple_page.route`` +decorator the blueprint will record the intention of registering the +function `show` on the application when it's later registered. +Additionally it will prefix the endpoint of the function with the +name of the blueprint which was given to the :class:`Blueprint` +constructor (in this case also ``simple_page``). + +So how do you register that blueprint? Like this:: + + from flask import Flask + from yourapplication.simple_page import simple_page + + app = Flask(__name__) + app.register_blueprint(simple_page) + +If you check the rules registered on the application, you will find +these:: + + [' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> simple_page.show>, + simple_page.show>] + +The first one is obviously from the application ifself for the static +files. The other two are for the `show` function of the ``simple_page`` +blueprint. As you can see, they are also prefixed with the name of the +blueprint and separated by a dot (``.``). + +Blueprint Resources +------------------- + +Blueprints can provide resources as well. Sometimes you might want to +introduce a blueprint only for the resources it provides. + +Blueprint Resource Folder +````````````````````````` + +Like for regular applications, blueprints are considered to be contained +in a folder. While multiple blueprints can origin from the same folder, +it does not have to be the case and it's usually not recommended. + +The folder is infered from the second argument to :class:`Blueprint` which +is ususally `__name__`. This argument specifies what logical Python +module or package corresponds to the blueprint. If it points to an actual +Python package that package (which is a folder on the filesystem) is the +resource folder. If it's a module, the package the module is contained in +will be the resource folder. You can access the +:attr:`Blueprint.root_path` property to see what's the resource folder:: + + >>> simple_page.root_path + '/Users/username/TestProject/yourapplication' + +To quickly open sources from this folder you can use the +:meth:`~Blueprint.open_resource` function:: + + with simple_page.open_resource('static/style.css') as f: + code = f.read() + +Static Files +```````````` + +A blueprint can expose a folder with static files by providing a path to a +folder on the filesystem via the `static_folder` keyword argument. It can +either be an absolute path or one relative to the folder of the +blueprint:: + + admin = Blueprint('admin', __name__, static_folder='static') + +By default the rightmost part of the path is where it is exposed on the +web. Because the folder is called ``static`` here it will be available at +the location of the blueprint + ``/static``. Say the blueprint is +registered for ``/admin`` the static folder will be at ``/admin/static``. + +The endpoint is named `blueprint_name.static` so you can generate URLs to +it like you would do to the static folder of the application:: + + url_for('admin.static', filename='style.css') + +Templates +````````` + +If you want the blueprint to expose templates you can do that by providing +the `template_folder` parameter to the :class:`Blueprint` constructor:: + + admin = Blueprint('admin', __name__, template_folder='templates') + +As for static files, the path can be absolute or relative to the blueprint +resource folder. From abe1378caec759cc8c6fc0afeaad248677c4420f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 16 Jun 2011 23:55:49 +0200 Subject: [PATCH 0634/3747] Chnaged a bunch of behavior in blueprints for it to be more flexible. Improved backwards compat. --- flask/app.py | 7 +++-- flask/blueprints.py | 6 ++-- flask/helpers.py | 11 ++++--- flask/module.py | 3 +- flask/templating.py | 75 +++++++++++++++++++++----------------------- tests/flask_tests.py | 2 +- 6 files changed, 50 insertions(+), 54 deletions(-) diff --git a/flask/app.py b/flask/app.py index b76e69e6..41eea79d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -206,8 +206,9 @@ class Flask(_PackageBoundObject): test_client_class = None def __init__(self, import_name, static_path=None, static_url_path=None, - static_folder='static'): - _PackageBoundObject.__init__(self, import_name) + static_folder='static', template_folder='templates'): + _PackageBoundObject.__init__(self, import_name, + template_folder=template_folder) if static_path is not None: from warnings import warn warn(DeprecationWarning('static_path is now called ' @@ -456,7 +457,7 @@ class Flask(_PackageBoundObject): rv.filters['tojson'] = _tojson_filter return rv - def create_jinja_loader(self): + def create_global_jinja_loader(self): """Creates the loader for the Jinja2 environment. Can be used to override just the loader and keeping the rest unchanged. diff --git a/flask/blueprints.py b/flask/blueprints.py index 41d5ef95..7afcc8a8 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -56,9 +56,9 @@ class Blueprint(_PackageBoundObject): _got_registered_once = False def __init__(self, name, import_name, static_folder=None, - static_url_path=None, url_prefix=None, - subdomain=None): - _PackageBoundObject.__init__(self, import_name) + static_url_path=None, template_folder=None, + url_prefix=None, subdomain=None): + _PackageBoundObject.__init__(self, import_name, template_folder) self.name = name self.url_prefix = url_prefix self.subdomain = subdomain diff --git a/flask/helpers.py b/flask/helpers.py index eff32273..09e8620e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -485,13 +485,15 @@ class locked_cached_property(object): class _PackageBoundObject(object): - template_folder = 'templates' - - def __init__(self, import_name): + def __init__(self, import_name, template_folder=None): #: The name of the package or module. Do not change this once #: it was set by the constructor. self.import_name = import_name + #: location of the templates. `None` if templates should not be + #: exposed. + self.template_folder = template_folder + #: Where is the app root located? self.root_path = _get_package_path(self.import_name) @@ -544,8 +546,7 @@ class _PackageBoundObject(object): """ if not self.has_static_folder: raise RuntimeError('No static folder for this object') - return send_from_directory(os.path.join(self.root_path, 'static'), - filename) + return send_from_directory(self.static_folder, filename) def open_resource(self, resource): """Opens a resource from the application's resource folder. To see diff --git a/flask/module.py b/flask/module.py index 2e9b9db4..61b3cbc4 100644 --- a/flask/module.py +++ b/flask/module.py @@ -11,7 +11,6 @@ import os -from .helpers import _PackageBoundObject, _endpoint_from_view_func from .blueprints import Blueprint @@ -37,7 +36,7 @@ class Module(Blueprint): 'does not point to a submodule' name = import_name.rsplit('.', 1)[1] Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, - subdomain=subdomain) + subdomain=subdomain, template_folder='templates') if os.path.isdir(os.path.join(self.root_path, 'static')): self._static_folder = 'static' diff --git a/flask/templating.py b/flask/templating.py index 5800bf83..d50691b3 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -38,69 +38,64 @@ class Environment(BaseEnvironment): def __init__(self, app, **options): if 'loader' not in options: - options['loader'] = app.create_jinja_loader() + options['loader'] = app.create_global_jinja_loader() BaseEnvironment.__init__(self, **options) self.app = app - def join_path(self, template, parent): - if template and template[0] == ':': - template = parent.split(':', 1)[0] + template - return template - class DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all - the module folders. + the blueprint folders. """ def __init__(self, app): self.app = app def get_source(self, environment, template): - # newstyle template support. blueprints are explicit and no further - # magic is involved. If the template cannot be loaded by the - # blueprint loader it just gives up, no further steps involved. - if ':' in template: - blueprint_name, local_template = template.split(':', 1) - local_template = posixpath.normpath(local_template) - blueprint = self.app.blueprints.get(blueprint_name) - if blueprint is None: - raise TemplateNotFound(template) - loader = blueprint.jinja_loader - if loader is not None: - return loader.get_source(environment, local_template) + for loader, local_name in self._iter_loaders(template): + try: + return loader.get_source(environment, local_name) + except TemplateNotFound: + pass - # if modules are enabled we call into the old style template lookup - # and try that before we go with the real deal. - loader = None + raise TemplateNotFound(template) + + def _iter_loaders(self, template): + loader = self.app.jinja_loader + if loader is not None: + yield loader, template + + # old style module based loaders in case we are dealing with a + # blueprint that is an old style module try: - module, name = posixpath.normpath(template).split('/', 1) + module, local_name = posixpath.normpath(template).split('/', 1) blueprint = self.app.blueprints[module] if blueprint_is_module(blueprint): loader = blueprint.jinja_loader - except (ValueError, KeyError, TemplateNotFound): - pass - try: - if loader is not None: - return loader.get_source(environment, name) - except TemplateNotFound: + if loader is not None: + yield loader, local_name + except (ValueError, KeyError): pass - # at the very last, load templates from the environment - return self.app.jinja_loader.get_source(environment, template) + for blueprint in self.app.blueprints.itervalues(): + loader = blueprint.jinja_loader + if loader is not None: + yield loader, template def list_templates(self): - result = set(self.app.jinja_loader.list_templates()) - - for name, module in self.app.modules.iteritems(): - if module.jinja_loader is not None: - for template in module.jinja_loader.list_templates(): - result.add('%s/%s' % (name, template)) + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) for name, blueprint in self.app.blueprints.iteritems(): - if blueprint.jinja_loader is not None: - for template in blueprint.jinja_loader.list_templates(): - result.add('%s:%s' % (name, template)) + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + prefix = '' + if not blueprint_is_module(blueprint): + prefix = name + '/' + result.add(prefix + template) return list(result) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7c430417..75da40d5 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -992,7 +992,7 @@ class TemplatingTestCase(unittest.TestCase): def test_custom_template_loader(self): class MyFlask(flask.Flask): - def create_jinja_loader(self): + def create_global_jinja_loader(self): from jinja2 import DictLoader return DictLoader({'index.html': 'Hello Custom World!'}) app = MyFlask(__name__) From 62d91d67f3b96ce7412d747ab3dd60a3aecc8276 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 02:45:57 +0200 Subject: [PATCH 0635/3747] More documentation updates --- docs/blueprints.rst | 28 +++++++++++++++++++++++++++- flask/app.py | 7 ++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 8a83128d..d1ee7963 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -147,4 +147,30 @@ the `template_folder` parameter to the :class:`Blueprint` constructor:: admin = Blueprint('admin', __name__, template_folder='templates') As for static files, the path can be absolute or relative to the blueprint -resource folder. +resource folder. The template folder is added to the searchpath of +templates but with a lower priority than the actual application's template +folder. That way you can easily override templates that a blueprint +provides in the actual application. + +So if you have a blueprint in the folder ``yourapplication/admin`` and you +want to render the template ``'admin/index.html'`` and you have provided +``templates`` as a `template_folder` you will have to create a file like +this: ``yourapplication/admin/templates/admin/index.html``. + +Building URLs +------------- + +If you want to link from one page to another you can use the +:func:`url_for` function just like you normally would do just that you +prefix the URL endpoint with the name of the blueprint and a dot (``.``):: + + url_for('admin.index') + +Additionally if you are in a view function of a blueprint or a rendered +template and you want to link to another endpoint of the same blueprint, +you can use relative redirects by prefixing the endpoint with a dot only:: + + url_for('.index') + +This will link to ``admin.index`` for instance in case the current request +was dispatched to any other admin blueprint endpoint. diff --git a/flask/app.py b/flask/app.py index 41eea79d..dcc6daed 100644 --- a/flask/app.py +++ b/flask/app.py @@ -459,7 +459,12 @@ class Flask(_PackageBoundObject): def create_global_jinja_loader(self): """Creates the loader for the Jinja2 environment. Can be used to - override just the loader and keeping the rest unchanged. + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`create_jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. .. versionadded:: 0.7 """ From dbfa04ece6f07a8cde3dac672b3d3d63b941070f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 03:29:31 +0200 Subject: [PATCH 0636/3747] Removed old module documentation and added some new for blueprints. --- docs/blueprints.rst | 17 ++++ docs/patterns/packages.rst | 187 ++----------------------------------- 2 files changed, 23 insertions(+), 181 deletions(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index d1ee7963..7d8ccc56 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -68,6 +68,9 @@ Additionally it will prefix the endpoint of the function with the name of the blueprint which was given to the :class:`Blueprint` constructor (in this case also ``simple_page``). +Registering Blueprints +---------------------- + So how do you register that blueprint? Like this:: from flask import Flask @@ -88,6 +91,20 @@ files. The other two are for the `show` function of the ``simple_page`` blueprint. As you can see, they are also prefixed with the name of the blueprint and separated by a dot (``.``). +Blueprints however can also be mounted at different locations:: + + app.register_blueprint(simple_page, url_prefix='/pages') + +And sure enough, these are the generated rules:: + + [' (HEAD, OPTIONS, GET) -> static>, + ' (HEAD, OPTIONS, GET) -> simple_page.show>, + simple_page.show>] + +On top of that you can register blueprints multiple times though not every +blueprint might respond properly to that. In fact it depends on how the +blueprint is implemented if it can be mounted more than once. + Blueprint Resources ------------------- diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 28cd70e4..4cd9dd27 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -105,185 +105,10 @@ You should then end up with something like that:: .. _working-with-modules: -Working with Modules --------------------- +Working with Blueprints +----------------------- -For larger applications with more than a dozen views it makes sense to -split the views into modules. First let's look at the typical structure of -such an application:: - - /yourapplication - /yourapplication - /__init__.py - /views - __init__.py - admin.py - frontend.py - /static - /style.css - /templates - layout.html - index.html - login.html - ... - -The views are stored in the `yourapplication.views` package. Just make -sure to place an empty `__init__.py` file in there. Let's start with the -`admin.py` file in the view package. - -First we have to create a :class:`~flask.Module` object with the name of -the package. This works very similar to the :class:`~flask.Flask` object -you have already worked with, it just does not support all of the methods, -but most of them are the same. - -Long story short, here's a nice and concise example:: - - from flask import Module - - admin = Module(__name__) - - @admin.route('/') - def index(): - pass - - @admin.route('/login') - def login(): - pass - - @admin.route('/logout') - def logout(): - pass - -Do the same with the `frontend.py` and then make sure to register the -modules in the application (`__init__.py`) like this:: - - from flask import Flask - app = Flask(__name__) - - from yourapplication.views.admin import admin - from yourapplication.views.frontend import frontend - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - -We register the modules with the app so that it can add them to the -URL map for our application. Note the prefix argument to the admin -module: by default when we register a module, that module's end-points -will be registered on `/` unless we specify this argument. - -So what is different when working with modules? It mainly affects URL -generation. Remember the :func:`~flask.url_for` function? When not -working with modules it accepts the name of the function as first -argument. This first argument is called the "endpoint". When you are -working with modules you can use the name of the function like you did -without, when generating modules from a function or template in the same -module. If you want to generate the URL to another module, prefix it with -the name of the module and a dot. - -Confused? Let's clear that up with some examples. Imagine you have a -method in one module (say `admin`) and you want to redirect to a -different module (say `frontend`). This would look like this:: - - @admin.route('/to_frontend') - def to_frontend(): - return redirect(url_for('frontend.index')) - - @frontend.route('/') - def index(): - return "I'm the frontend index" - -Now let's say we only want to redirect to a different function in the same -module. Then we can either use the full qualified endpoint name like we -did in the example above, or we just use the function name:: - - @frontend.route('/to_index') - def to_index(): - return redirect(url_for('index')) - - @frontend.route('/') - def index(): - return "I'm the index" - -.. _modules-and-resources: - -Modules and Resources ---------------------- - -.. versionadded:: 0.5 - -If a module is located inside an actual Python package it may contain -static files and templates. Imagine you have an application like this:: - - - /yourapplication - __init__.py - /apps - __init__.py - /frontend - __init__.py - views.py - /static - style.css - /templates - index.html - about.html - ... - /admin - __init__.py - views.py - /static - style.css - /templates - list_items.html - show_item.html - ... - -The static folders automatically become exposed as URLs. For example if -the `admin` module is exported with an URL prefix of ``/admin`` you can -access the style css from its static folder by going to -``/admin/static/style.css``. The URL endpoint for the static files of the -admin would be ``'admin.static'``, similar to how you refer to the regular -static folder of the whole application as ``'static'``. - -If you want to refer to the templates you just have to prefix it with the -name of the module. So for the admin it would be -``render_template('admin/list_items.html')`` and so on. It is not -possible to refer to templates without the prefixed module name. This is -explicit unlike URL rules. Also with the move of the views into from -`yourapplication.views.admin` too `yourapplication.apps.admin.views` you -will have to give the module an explit shortname. Why? Because otherwise -all your modules will be internally known as `views` which is obviously -not what you want:: - - # in yourapplication/apps/admin/views.py - admin = Module(__name__, 'admin') - -The setup code changes slightly because of the imports:: - - # in yourapplication/__init__.py - from flask import Flask - from yourapplication.apps.admin.views import admin - from yourapplication.apps.frontend.views import frontend - - app = Flask(__name__) - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - -.. admonition:: References to Static Folders - - Please keep in mind that if you are using unqualified endpoints by - default Flask will always assume the module's static folder, even if - there is no such folder. - - If you want to refer to the application's static folder, use a leading - dot:: - - # this refers to the application's static folder - url_for('.static', filename='static.css') - - # this refers to the current module's static folder - url_for('static', filename='static.css') - - This is the case for all endpoints, not just static folders, but for - static folders it's more common that you will stumble upon this because - most applications will have a static folder in the application and not - a specific module. +If you have larger applications it's recommended to divide them into +smaller groups where each group is implemented with the help of a +blueprint. For a gentle introduction into this topic refer to the +:ref:`blueprints` chapter of the documentation. From e17e74d3a74b5d32a622587b2b9da9840bf3a7e0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 03:29:40 +0200 Subject: [PATCH 0637/3747] Started work on testcases for blueprints --- flask/blueprints.py | 18 +++++++++++++++--- tests/flask_tests.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index 7afcc8a8..2c45248a 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -9,7 +9,6 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -import os from functools import update_wrapper from .helpers import _PackageBoundObject, _endpoint_from_view_func @@ -36,14 +35,24 @@ class BlueprintSetupState(object): url_prefix = self.blueprint.url_prefix self.url_prefix = url_prefix + self.url_defaults = dict(self.blueprint.url_defaults) + self.url_defaults.update(self.options.get('url_defaults', ())) + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ if self.url_prefix: rule = self.url_prefix + rule options.setdefault('subdomain', self.subdomain) if endpoint is None: endpoint = _endpoint_from_view_func(view_func) + defaults = self.url_defaults + if 'defaults' in options: + defaults = dict(defaults, **options.pop('defaults')) self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), - view_func, **options) + view_func, defaults=defaults, **options) class Blueprint(_PackageBoundObject): @@ -57,7 +66,7 @@ class Blueprint(_PackageBoundObject): def __init__(self, name, import_name, static_folder=None, static_url_path=None, template_folder=None, - url_prefix=None, subdomain=None): + url_prefix=None, subdomain=None, url_defaults=None): _PackageBoundObject.__init__(self, import_name, template_folder) self.name = name self.url_prefix = url_prefix @@ -66,6 +75,9 @@ class Blueprint(_PackageBoundObject): self.static_url_path = static_url_path self.deferred_functions = [] self.view_functions = {} + if url_defaults is None: + url_defaults = {} + self.url_defaults = url_defaults def record(self, func): """Registers a function that is called when the blueprint is diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 75da40d5..75ffa095 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1272,6 +1272,27 @@ class BlueprintTestCase(unittest.TestCase): assert c.get('/backend-no').data == 'backend says no' assert c.get('/what-is-a-sideend').data == 'application itself says no' + def test_blueprint_url_definitions(self): + bp = flask.Blueprint('test', __name__) + + @bp.route('/foo', defaults={'baz': 42}) + def foo(bar, baz): + return '%s/%d' % (bar, baz) + + @bp.route('/bar') + def bar(bar): + return unicode(bar) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) + app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) + + c = app.test_client() + self.assertEqual(c.get('/1/foo').data, u'23/42') + self.assertEqual(c.get('/2/foo').data, u'19/42') + self.assertEqual(c.get('/1/bar').data, u'23') + self.assertEqual(c.get('/2/bar').data, u'19') + class SendfileTestCase(unittest.TestCase): From 37fab7888744b23f62f678614656b686bb046379 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 03:39:49 +0200 Subject: [PATCH 0638/3747] Added a migrated moduleapp as blueprint app --- tests/blueprintapp/__init__.py | 7 ++++ tests/blueprintapp/apps/__init__.py | 0 tests/blueprintapp/apps/admin/__init__.py | 13 +++++++ .../apps/admin/static/css/test.css | 1 + tests/blueprintapp/apps/admin/static/test.txt | 1 + .../apps/admin/templates/admin/index.html | 1 + tests/blueprintapp/apps/frontend/__init__.py | 8 +++++ .../frontend/templates/frontend/index.html | 1 + tests/flask_tests.py | 36 +++++++++++++++++-- 9 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 tests/blueprintapp/__init__.py create mode 100644 tests/blueprintapp/apps/__init__.py create mode 100644 tests/blueprintapp/apps/admin/__init__.py create mode 100644 tests/blueprintapp/apps/admin/static/css/test.css create mode 100644 tests/blueprintapp/apps/admin/static/test.txt create mode 100644 tests/blueprintapp/apps/admin/templates/admin/index.html create mode 100644 tests/blueprintapp/apps/frontend/__init__.py create mode 100644 tests/blueprintapp/apps/frontend/templates/frontend/index.html diff --git a/tests/blueprintapp/__init__.py b/tests/blueprintapp/__init__.py new file mode 100644 index 00000000..fa76807c --- /dev/null +++ b/tests/blueprintapp/__init__.py @@ -0,0 +1,7 @@ +from flask import Flask + +app = Flask(__name__) +from moduleapp.apps.admin import admin +from moduleapp.apps.frontend import frontend +app.register_blueprint(admin) +app.register_blueprint(frontend) diff --git a/tests/blueprintapp/apps/__init__.py b/tests/blueprintapp/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/blueprintapp/apps/admin/__init__.py b/tests/blueprintapp/apps/admin/__init__.py new file mode 100644 index 00000000..fe33e3e9 --- /dev/null +++ b/tests/blueprintapp/apps/admin/__init__.py @@ -0,0 +1,13 @@ +from flask import Blueprint, render_template + +admin = Blueprint(__name__, url_prefix='/admin') + + +@admin.route('/') +def index(): + return render_template('admin/index.html') + + +@admin.route('/index2') +def index2(): + return render_template('./admin/index.html') diff --git a/tests/blueprintapp/apps/admin/static/css/test.css b/tests/blueprintapp/apps/admin/static/css/test.css new file mode 100644 index 00000000..b9f564de --- /dev/null +++ b/tests/blueprintapp/apps/admin/static/css/test.css @@ -0,0 +1 @@ +/* nested file */ diff --git a/tests/blueprintapp/apps/admin/static/test.txt b/tests/blueprintapp/apps/admin/static/test.txt new file mode 100644 index 00000000..f220d22f --- /dev/null +++ b/tests/blueprintapp/apps/admin/static/test.txt @@ -0,0 +1 @@ +Admin File diff --git a/tests/blueprintapp/apps/admin/templates/admin/index.html b/tests/blueprintapp/apps/admin/templates/admin/index.html new file mode 100644 index 00000000..eeec199a --- /dev/null +++ b/tests/blueprintapp/apps/admin/templates/admin/index.html @@ -0,0 +1 @@ +Hello from the Admin diff --git a/tests/blueprintapp/apps/frontend/__init__.py b/tests/blueprintapp/apps/frontend/__init__.py new file mode 100644 index 00000000..e98ff280 --- /dev/null +++ b/tests/blueprintapp/apps/frontend/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template + +frontend = Blueprint(__name__) + + +@frontend.route('/') +def index(): + return render_template('frontend/index.html') diff --git a/tests/blueprintapp/apps/frontend/templates/frontend/index.html b/tests/blueprintapp/apps/frontend/templates/frontend/index.html new file mode 100644 index 00000000..a062d713 --- /dev/null +++ b/tests/blueprintapp/apps/frontend/templates/frontend/index.html @@ -0,0 +1 @@ +Hello from the Frontend diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 75ffa095..9a0905c1 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -433,7 +433,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): def test_teardown_request_handler_debug_mode(self): called = [] app = flask.Flask(__name__) - app.debug = True + app.testing = True @app.teardown_request def teardown_request(exc): called.append(True) @@ -819,7 +819,7 @@ class JSONTestCase(unittest.TestCase): def test_json_body_encoding(self): app = flask.Flask(__name__) - app.debug = True + app.testing = True @app.route('/') def index(): return flask.request.json @@ -1139,7 +1139,7 @@ class ModuleTestCase(unittest.TestCase): def test_templates_and_static(self): app = moduleapp - app.debug = True + app.testing = True c = app.test_client() rv = c.get('/') @@ -1293,6 +1293,36 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/1/bar').data, u'23') self.assertEqual(c.get('/2/bar').data, u'19') + def test_templates_and_static(self): + from blueprintapp import app + c = app.test_client() + + rv = c.get('/') + assert rv.data == 'Hello from the Frontend' + rv = c.get('/admin/') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/static/test.txt') + assert rv.data.strip() == 'Admin File' + rv = c.get('/admin/static/css/test.css') + assert rv.data.strip() == '/* nested file */' + + with app.test_request_context(): + assert flask.url_for('admin.static', filename='test.txt') \ + == '/admin/static/test.txt' + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + assert e.name == 'missing.html' + else: + assert 0, 'expected exception' + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template('nested/nested.txt') == 'I\'m nested' + class SendfileTestCase(unittest.TestCase): From 0fcbe0a47895a1d1106b50420fe696e04520b621 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 03:42:11 +0200 Subject: [PATCH 0639/3747] render_template calls no longer need changing for blueprints. They need new paths though :( --- scripts/flask-07-upgrade.py | 75 +++---------------------------------- 1 file changed, 6 insertions(+), 69 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 5df500c4..7559f7a3 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -21,7 +21,6 @@ """ import re import os -import sys import inspect import difflib import posixpath @@ -177,37 +176,7 @@ def rewrite_blueprint_imports(contents): return ''.join(new_file) -def rewrite_render_template_calls(contents, module_declarations): - mapping = dict(module_declarations) - annotated_lines = [] - - def make_line_annotations(): - if not annotated_lines: - last_index = 0 - for line in contents.splitlines(True): - last_index += len(line) - annotated_lines.append((last_index, line)) - - def backtrack_module_name(call_start): - make_line_annotations() - for idx, (line_end, line) in enumerate(annotated_lines): - if line_end > call_start: - for _, line in reversed(annotated_lines[:idx]): - match = _mod_route_re.search(line) - if match is not None: - shortname = match.group(1) - return mapping.get(shortname) - - def handle_match(match): - template_name = ast.literal_eval(match.group(2)) - modname = backtrack_module_name(match.start()) - if modname is not None and template_name.startswith(modname + '/'): - template_name = modname + ':' + template_name[len(modname) + 1:] - return match.group(1) + repr(template_name) - return _render_template_re.sub(handle_match, contents) - - -def rewrite_for_blueprints(contents, filename, template_bundles=False): +def rewrite_for_blueprints(contents, filename): modules_declared = [] def handle_match(match): target = match.group(1) @@ -222,21 +191,17 @@ def rewrite_for_blueprints(contents, filename, template_bundles=False): if modules_declared: new_contents = rewrite_blueprint_imports(new_contents) - if template_bundles: - new_contents = rewrite_render_template_calls(new_contents, - modules_declared) for pattern, replacement in _blueprint_related: new_contents = pattern.sub(replacement, new_contents) return new_contents -def upgrade_python_file(filename, contents, teardown, template_bundles): +def upgrade_python_file(filename, contents, teardown): new_contents = fix_url_for(contents) if teardown: new_contents = fix_teardown_funcs(new_contents) - new_contents = rewrite_for_blueprints(new_contents, filename, - template_bundles) + new_contents = rewrite_for_blueprints(new_contents, filename) new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]', new_contents) make_diff(filename, contents, new_contents) @@ -266,32 +231,16 @@ def walk_path(path): yield filename, 'template' -def scan_path(path=None, teardown=True, template_bundles=True): +def scan_path(path=None, teardown=True): for filename, type in walk_path(path): with open(filename) as f: contents = f.read() if type == 'python': - upgrade_python_file(filename, contents, teardown, - template_bundles) + upgrade_python_file(filename, contents, teardown) elif type == 'template': upgrade_template_file(filename, contents) -def autodetect_template_bundles(paths): - folders_with_templates = set() - for path in paths: - for filename, type in walk_path(path): - if type == 'template': - fullpath = filename.replace(os.path.sep, '/') - index = fullpath.find('/templates/') - if index >= 0: - folder = fullpath[:index] - else: - folder = posixpath.dirname(fullpath) - folders_with_templates.add(folder) - return len(folders_with_templates) > 1 - - def main(): """Entrypoint""" parser = OptionParser(usage='%prog [options] [paths]') @@ -302,10 +251,6 @@ def main(): action='store_true', help='Indicate to the system ' 'that templates are bundled with modules. Default ' 'is auto detect.') - parser.add_option('-B', '--no-bundled-templates', dest='no_bundled_tmpl', - action='store_true', help='Indicate to the system ' - 'that templates are not bundled with modules. ' - 'Default is auto detect') options, args = parser.parse_args() if not args: args = ['.'] @@ -315,16 +260,8 @@ def main(): 'The runtime requirements for Flask 0.7 however are still ' 'Python 2.5.') - if options.no_bundled_tmpl: - template_bundles = False - elif options.bundled_tmpl: - template_bundles = True - else: - template_bundles = autodetect_template_bundles(args) - for path in args: - scan_path(path, teardown=not options.no_teardown, - template_bundles=template_bundles) + scan_path(path, teardown=not options.no_teardown) if __name__ == '__main__': From dbb9a2ed46ddf75ea3101e88deaf61b8788ebc76 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 03:45:19 +0200 Subject: [PATCH 0640/3747] Added note on templates not being updated --- docs/upgrading.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 0a965d55..7fcef042 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -55,6 +55,14 @@ To apply the upgrade script do the following: patch -p1 < patchfile.diff +5. If you were using per-module template folders you need to move some + templates around. Previously if you had a folder named ``templates`` + next to a blueprint named ``admin`` the implicit template path + automatically was ``admin/index.html`` for a template file called + ``templates/index.html``. This no longer is the case. Now you need + to name the template ``templates/admin/index.html``. The tool will + not detect this so you will have to do that on your own. + Please note that deprecation warnings are disabled by default starting with Python 2.7. In order to see the deprecation warnings that might be emitted you have to enabled them with the :mod:`warnings` module. @@ -66,7 +74,6 @@ like svn, hg or git have builtin support for applying unified diffs as generated by the tool. Check the manual of your version control system for more information. - Bug in Request Locals ````````````````````` From 86175054d63df56c4f3c79108d363ef5e127e440 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 20:27:25 +0200 Subject: [PATCH 0641/3747] More docstrings for blueprints. --- docs/api.rst | 9 ++++++--- docs/patterns/appfactories.rst | 4 ++-- docs/quickstart.rst | 3 +-- flask/blueprints.py | 35 ++++++++++++++++++++++++++++++---- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index b3953537..62f5e01d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -18,10 +18,10 @@ Application Object :inherited-members: -Module Objects --------------- +Blueprint Objects +----------------- -.. autoclass:: Module +.. autoclass:: Blueprint :members: :inherited-members: @@ -350,6 +350,9 @@ Useful Internals if ctx is not None: return ctx.session +.. autoclass:: flask.blueprints.BlueprintSetupState + :members: + Signals ------- diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 134ffcf4..09884790 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -3,8 +3,8 @@ Application Factories ===================== -If you are already using packages and modules for your application -(:ref:`packages`) there are a couple of really nice ways to further improve +If you are already using packages and blueprints for your application +(:ref:`blueprints`) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when the module is imported. But if you move the creation of this object, into a function, you can then create multiple instances of this and later. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fc5ae0f1..3875b296 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -381,8 +381,7 @@ package it's actually inside your package: /hello.html For templates you can use the full power of Jinja2 templates. Head over -to the :ref:`templating` section of the documentation or the official -`Jinja2 Template Documentation +to the the official `Jinja2 Template Documentation `_ for more information. Here is an example template: diff --git a/flask/blueprints.py b/flask/blueprints.py index 181f0c16..4a3f531a 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -16,25 +16,46 @@ from .helpers import _PackageBoundObject, _endpoint_from_view_func class BlueprintSetupState(object): """Temporary holder object for registering a blueprint with the - application. + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. """ def __init__(self, blueprint, app, options, first_registration): + #: a reference to the current application self.app = app + + #: a reference to the blurprint that created this setup state. self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. self.first_registration = first_registration subdomain = self.options.get('subdomain') if subdomain is None: subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, `None` + #: otherwise. self.subdomain = subdomain url_prefix = self.options.get('url_prefix') if url_prefix is None: url_prefix = self.blueprint.url_prefix + + #: The prefix that should be used for all URLs defined on the + #: blueprint. self.url_prefix = url_prefix + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. self.url_defaults = dict(self.blueprint.url_defaults) self.url_defaults.update(self.options.get('url_defaults', ())) @@ -56,7 +77,11 @@ class BlueprintSetupState(object): class Blueprint(_PackageBoundObject): - """Represents a blueprint. + """Represents a blueprint. A blueprint is an object that records + functions that will be called with the + :class:`~flask.blueprint.BlueprintSetupState` later to register functions + or other things on the main application. See :ref:`blueprints` for more + information. .. versionadded:: 0.7 """ @@ -104,6 +129,10 @@ class Blueprint(_PackageBoundObject): return self.record(update_wrapper(wrapper, func)) def make_setup_state(self, app, options, first_registration=False): + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ return BlueprintSetupState(self, app, options, first_registration) def register(self, app, options, first_registration=False): @@ -254,8 +283,6 @@ class Blueprint(_PackageBoundObject): Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator of the :class:`~flask.Flask` object. - - .. versionadded:: 0.7 """ def decorator(f): self.record_once(lambda s: s.app._register_error_handler( From d62e277dfec0f374d72b01ca5d73985ac342d3f2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 21:33:31 +0200 Subject: [PATCH 0642/3747] Added pattern for url processors because the API documentation itself might not reveal much. --- docs/patterns/index.rst | 1 + docs/patterns/urlprocessors.rst | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/patterns/urlprocessors.rst diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index ed231163..4233808b 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -19,6 +19,7 @@ Snippet Archives `_. packages appfactories appdispatch + urlprocessors distribute fabric sqlite3 diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst new file mode 100644 index 00000000..f00211a6 --- /dev/null +++ b/docs/patterns/urlprocessors.rst @@ -0,0 +1,126 @@ +Using URL Processors +==================== + +.. versionadded:: 0.7 + +Flask 0.7 introduces the concept of URL processors. The idea is that you +might have a bunch of resources with common parts in the URL that you +don't always explicitly want to provide. For instance you might have a +bunch of URLs that have the language code in it but you don't want to have +to handle it in every single function yourself. + +URL processors are especially helpful when combined with blueprints. We +will handle both application specific URL processors here as well as +blueprint specifics. + +Internationalized Application URLs +---------------------------------- + +Consider an application like this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.route('//') + def index(lang_code): + g.lang_code = lang_code + ... + + @app.route('//about') + def about(lang_code): + g.lang_code = lang_code + ... + +This is an awful lot of reptition as you have to handle the language code +setting on the :data:`~flask.g` object yourself in every single function. +Sure, a decorator could be used to simplify this, but if you want to +generate URLs from one function to another you would have to still provide +the language code explicitly which can be annoying. + +For the latter, this is where :func:`~flask.Flask.url_defaults` functions +come in. They can automatically inject values into a call for +:func:`~flask.url_for` automatically. The code below checks if the +language code is not yet in the dictionary of URL values and if the +endpoint wants a value named ``'lang_code'``:: + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + +The method :meth:`~werkzeug.routing.Map.is_endpoint_expecting` of the URL +map can be used to figure out if it would make sense to provide a language +code for the given endpoint. + +The reverse of that function are +:meth:`~flask.Flask.url_value_preprocessor`\s. They are executed right +after the request was matched and can execute code based on the URL +values. The idea is that they pull information out of the values +dictionary and put it somewhere else:: + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code', None) + +That way you no longer have to do the `lang_code` assigment to +:data:`~flask.g` in every function. You can further improve that by +writing your own decorator that prefixes URLs with the language code, but +the more beautiful solution is using a blueprint. Once the +``'lang_code'`` is popped from the values dictionary and it will no longer +be forwarded to the view function reducing the code to this:: + + from flask import Flask, g + + app = Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if 'lang_code' in values or not g.lang_code: + return + if app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values['lang_code'] = g.lang_code + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + ... + + @app.route('//about') + def about(): + ... + +Internationalized Blueprint URLs +-------------------------------- + +Because blueprints can automatically prefix all URLs with a common string +it's easy to automatically do that for every function. Furthermore +blueprints can have per-blueprint URL processors which removes a whole lot +of logic from the :meth:`~flask.Flask.url_defaults` function because it no +longer has to check if the URL is really interested in a ``'lang_code'`` +parameter:: + + from flask import Blueprint, g + + bp = Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + ... + + @bp.route('/about') + def about(): + ... From 7e55b5084ddbaeeadd8358b216f36e7706c79199 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 21:53:11 +0200 Subject: [PATCH 0643/3747] Mention the TESTING flag in the docs --- docs/testing.rst | 24 ++++++++++++++++++++++-- examples/flaskr/flaskr_tests.py | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 48af2a18..a196e2b7 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -41,6 +41,7 @@ In order to test the application, we add a second module def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True self.app = flaskr.app.test_client() flaskr.init_db() @@ -55,7 +56,10 @@ The code in the :meth:`~unittest.TestCase.setUp` method creates a new test client and initializes a new database. This function is called before each individual test function is run. To delete the database after the test, we close the file and remove it from the filesystem in the -:meth:`~unittest.TestCase.tearDown` method. +:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the +``TESTING`` config flag is activated. What it does is disabling the error +catching during request handling so that you get better error reports when +performing test requests against the application. This test client will give us a simple interface to the application. We can trigger test requests to the application, and the client will also keep track @@ -215,6 +219,23 @@ If you want to test your application with different configurations and there does not seem to be a good way to do that, consider switching to application factories (see :ref:`app-factories`). +Note however that if you are using a test request context, the +:meth:`~flask.Flask.before_request` functions are not automatically called +same fore :meth:`~flask.Flask.after_request` functions. However +:meth:`~flask.Flask.teardown_request` functions are indeed executed when +the test request context leaves the `with` block. If you do want the +:meth:`~flask.Flask.before_request` functions to be called as well, you +need to call :meth:`~flask.Flask.preprocess_request` yourself:: + + app = flask.Flask(__name__) + + with app.test_request_context('/?name=Peter'): + app.preprocess_request() + ... + +This can be necessary to open database connections or something similar +depending on how your application was designed. + Keeping the Context Around -------------------------- @@ -238,4 +259,3 @@ is no longer available (because you are trying to use it outside of the actual r However, keep in mind that any :meth:`~flask.Flask.after_request` functions are already called at this point so your database connection and everything involved is probably already closed down. - diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index 06bf1035..cfac3782 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -19,6 +19,7 @@ class FlaskrTestCase(unittest.TestCase): def setUp(self): """Before each test, set up a blank database""" self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True self.app = flaskr.app.test_client() flaskr.init_db() From d38c61f42a0d0a9827322ac33c6a2ca4eaf240c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 17 Jun 2011 21:58:03 +0200 Subject: [PATCH 0644/3747] More modules -> blueprints --- docs/config.rst | 2 +- docs/patterns/appfactories.rst | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index d6a8278a..2487e8b7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -168,7 +168,7 @@ a little harder. There is no one 100% solution for this problem in general, but there are a couple of things you can do to improve that experience: -1. create your application in a function and register modules on it. +1. create your application in a function and register blueprints on it. That way you can create multiple instances of your application with different configurations attached which makes unittesting a lot easier. You can use this to pass in configuration as needed. diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 09884790..e2bb46e7 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -6,7 +6,7 @@ Application Factories If you are already using packages and blueprints for your application (:ref:`blueprints`) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when -the module is imported. But if you move the creation of this object, +the blueprint is imported. But if you move the creation of this object, into a function, you can then create multiple instances of this and later. So why would you want to do this? @@ -32,18 +32,18 @@ The idea is to set up the application in a function. Like this:: from yourapplication.views.admin import admin from yourapplication.views.frontend import frontend - app.register_module(admin) - app.register_module(frontend) + app.register_blueprint(admin) + app.register_blueprint(frontend) return app -The downside is that you cannot use the application object in the modules +The downside is that you cannot use the application object in the blueprints at import time. You can however use it from within a request. How do you get access the application with the config? Use :data:`~flask.current_app`:: - from flask import current_app, Module, render_template - admin = Module(__name__, url_prefix='/admin') + from flask import current_app, Blueprint, render_template + admin = Blueprint('admin', __name__, url_prefix='/admin') @admin.route('/') def index(): @@ -69,7 +69,7 @@ it. The following changes are straightforward and possible: 1. make it possible to pass in configuration values for unittests so that you don't have to create config files on the filesystem -2. call a function from a module when the application is setting up so +2. call a function from a blueprint when the application is setting up so that you have a place to modify attributes of the application (like hooking in before / after request handlers etc.) 3. Add in WSGI middlewares when the application is creating if necessary. From 04f2bbcb15bd7b3accd8e0820b83df1b2207aa24 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 20 Jun 2011 08:27:23 +0200 Subject: [PATCH 0645/3747] Updated JSON docs --- docs/security.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/security.rst b/docs/security.rst index 35afd49e..07a5b942 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -95,6 +95,13 @@ the form validation framework, which does not exist in Flask. JSON Security ------------- +.. admonition:: ECMAScript 5 Changes + + Starting with ECMAScript 5 the behavior of literals changed. Now they + are not constructed with the constructor of ``Array`` and others, but + with the builtin constructor of ``Array`` which closes this particular + attack vector. + JSON itself is a high-level serialization format, so there is barely anything that could cause security problems, right? You can't declare recursive structures that could cause problems and the only thing that From e6b9f509ba3c47f748d5c6da5f6de5500248dbfd Mon Sep 17 00:00:00 2001 From: Peter Manser Date: Mon, 20 Jun 2011 23:51:30 +0200 Subject: [PATCH 0646/3747] Improving documentation for loading jQuery. Now using Google CDN with fallback to local jQuery. --- docs/patterns/jquery.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index d4fe6d4e..f3c46e39 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -23,7 +23,7 @@ Loading jQuery In order to use jQuery, you have to download it first and place it in the static folder of your application and then ensure it's loaded. Ideally you have a layout template that is used for all pages where you just have -to add a script statement to your `head` to load jQuery: +to add a script statement to the bottom of your `` to load jQuery: .. sourcecode:: html @@ -35,15 +35,15 @@ Another method is using Google's `AJAX Libraries API .. sourcecode:: html - + + -In this case you don't have to put jQuery into your static folder, it will -instead be loaded from Google directly. This has the advantage that your +In this case you have to put jQuery into your static folder as a fallback, but it will +first try to load it directly from Google. This has the advantage that your website will probably load faster for users if they went to at least one other website before using the same jQuery version from Google because it -will already be in the browser cache. Downside is that if you don't have -network connectivity during development jQuery will not load. +will already be in the browser cache. Where is My Site? ----------------- From c40937e7bf1df492c99705733f6550f3f09baf91 Mon Sep 17 00:00:00 2001 From: wilsaj Date: Tue, 21 Jun 2011 10:05:26 -0500 Subject: [PATCH 0647/3747] Bug fix in the documentation for Deploying with Fabric: as of Fabric 1.0, in the local() function capture=False by default, so it must be explicitly set to capture=True so it returns a python string (http://docs.fabfile.org/en/1.0.1/api/core/operations.html#fabric.operations.local) --- docs/patterns/fabric.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index dbd4f913..b02ad277 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -48,7 +48,7 @@ virtual environment:: def deploy(): # figure out the release name and version - dist = local('python setup.py --fullname').strip() + dist = local('python setup.py --fullname', capture=True).strip() # upload the source tarball to the temporary folder on the server put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') # create a place where we can unzip the tarball, then enter From 239d9ccf5773c84a4f3faf7e415bb298892c2063 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 23 Jun 2011 08:42:27 -0400 Subject: [PATCH 0648/3747] Remove extreview .rst in favor of website listing. --- extreview/approved.rst | 168 ----------------------------------------- extreview/listed.rst | 64 ---------------- extreview/unlisted.rst | 55 -------------- 3 files changed, 287 deletions(-) delete mode 100644 extreview/approved.rst delete mode 100644 extreview/listed.rst delete mode 100644 extreview/unlisted.rst diff --git a/extreview/approved.rst b/extreview/approved.rst deleted file mode 100644 index 82bbf9f7..00000000 --- a/extreview/approved.rst +++ /dev/null @@ -1,168 +0,0 @@ -Approved Extensions -=================== - -This document contains a list of all extensions that were approved and the -date of approval as well as notes. This should make it possible to better -track the extension approval process. - - -Flask-Babel ------------ - -:First Approval: 2010-07-23 -:Last Review: 2010-07-23 -:Approved Version: 0.6 -:Approved License: BSD - -Notes: Developed by the Flask development head - -How to improve: add a better long description to the next release - - -Flask-SQLAlchemy ----------------- - -:First Approval: 2010-07-25 -:Last Review: 2010-07-25 -:Approved Version: 0.9.1 -:Approved License: BSD - -Notes: Developed by the Flask development head - -How to improve: add a better long description to the next release - - -Flask-Creole ------------- - -:First Approval: 2010-07-25 -:Last Review: 2010-07-25 -:Approved Version: 0.4.4 -:Approved License: BSD - -Notes: Flask-Markdown and this should share API, consider that when -approving Flask-Markdown - - -Flask-Genshi ------------- - -:First Approval: 2010-07-26 -:Last Review: 2010-07-26 -:Approved Version: 0.3.1 -:Approved License: BSD - -Notes: This is the first template engine extension. When others come -around it would be a good idea to decide on a common interface. - - -Flask-Script ------------- - -:First Approval: 2010-07-26 -:Last Review: 2010-07-26 -:Approved Version: 0.3 -:Approved License: BSD - -Notes: Flask-Actions has some overlap. Consider that when approving -Flask-Actions or similar packages. - - -Flask-CouchDB -------------- - -:First Approval: 2010-07-26 -:Last Review: 2010-07-26 -:Approved Version: 0.2.1 -:Approved License: MIT - -There is also Flask-CouchDBKit. Both are fine because they are doing -different things, but the latter is not yet approved. - - -Flask-Testing -------------- - -:First Approval: 2010-07-27 -:Last Review: 2010-07-27 -:Approved Version: 0.2.3 -:Approved License: BSD - -All fine. - - -Flask-WTF ---------- - -:First Approval: 2010-07-27 -:Last Review: 2010-07-27 -:Approved Version: 0.2.3 -:Approved License: BSD - -All fine. - - -Flask-Themes ------------- - -:First Approval: 2010-07-27 -:Last Review: 2010-07-27 -:Approved Version: 0.1.2 -:Approved License: MIT - -All fine. - - -Flask-Uploads -------------- - -:First Approval: 2010-07-27 -:Last Review: 2010-07-27 -:Approved Version: 0.1.2 -:Approved License: MIT - -All fine. - - -Flask-Mail ----------- - -:First Approval: 2010-07-29 -:Last Review: 2010-07-29 -:Approved Version: 0.3.4 -:Approved License: BSD - -All fine. - - -Flask-XML-RPC -------------- - -:First Approval: 2010-07-30 -:Last Review: 2010-07-30 -:Approved Version: 0.1.2 -:Approved License: MIT - -All fine. - - -Frozen-Flask ------------- - -:First Approval: 2011-06-05 -:Last Review: 2011-06-05 -:Approved Version: 0.4 -:Approved License: BSD - -All fine. Posted recommendations for minor items/enhancements to mailing list. - - -Flask-Login ------------ - -:First Approval: 2011-06-07 -:Last Review: 2011-06-07 -:Approved Version: 0.1 -:Approved License: MIT - -All fine. diff --git a/extreview/listed.rst b/extreview/listed.rst deleted file mode 100644 index 474213cb..00000000 --- a/extreview/listed.rst +++ /dev/null @@ -1,64 +0,0 @@ -Listed Extensions -================= - -This list contains extensions that passed listing. This means the -extension is on the list of extensions on the website. It does not -contain extensions that are approved. - - -Flask-CouchDBKit ----------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be fine for approval, but the test suite is not part of the sdist -package (missing entry in MANIFEST.in) and the test suite does not respond -to either "make test" or "python setup.py test". - - -flask-csrf ----------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.2 - -Will not be approved because this is functionality that should be handled -in the form handling systems which is for Flask-WTF already the case. -Also, this implementation only supports one open tab with forms. - -Name is not following Flask extension naming rules. - -Considered for unlisting. - - -flask-lesscss -------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.9.1 - -Broken package description, nonconforming package name, does not follow -standard API rules (init_lesscss instead of lesscss). - -Considered for unlisting, improved version should release as -"Flask-LessCSS" with a conforming API and fixed packages indices, as well -as a testsuite. - - -Flask-OAuth ------------ - -:Last-Review: 2010-07-25 -:Reviewed Version: 0.9 - -Short long description, missing tests. - - -Flask-OpenID ------------- - -:Last-Review: 2010-07-25 -:Reviewed Version: 1.0.1 - -Short long description, missing tests. diff --git a/extreview/unlisted.rst b/extreview/unlisted.rst deleted file mode 100644 index 6ee8b441..00000000 --- a/extreview/unlisted.rst +++ /dev/null @@ -1,55 +0,0 @@ -Unlisted Extensions -=================== - -This is a list of extensions that is currently rejected from listing and -with that also not approved. If an extension ends up here it should -improved to be listed. - - -Flask-Actions -------------- - -:Last Review: 2010-07-25 -:Reviewed Version: 0.2 - -Rejected because of missing description in PyPI, formatting issues with -the documentation (missing headlines, scrollbars etc.) and a general clash -of functionality with the Flask-Script package. Latter should not be a -problem, but the documentation should improve. For listing, the extension -developer should probably discuss the extension on the mailinglist with -others. - -Futhermore it also has an egg registered with an invalid filename. - - -Flask-Jinja2Extender --------------------- - -:Last Review: 2010-07-25 -:Reviewed Version: 0.1 - -Usecase not obvious, hacky implementation, does not solve a problem that -could not be solved with Flask itself. I suppose it is to aid other -extensions, but that should be discussed on the mailinglist. - - -Flask-Markdown --------------- - -:Last Review: 2010-07-25 -:Reviewed Version: 0.2 - -Would be great for enlisting but it should follow the API of Flask-Creole. -Besides that, the docstrings are not valid rst (run through rst2html to -see the issue) and it is missing tests. Otherwise fine :) - - -flask-urls ----------- - -:Last Review: 2010-07-25 -:Reviewed Version: 0.9.2 - -Broken PyPI index and non-conforming extension name. Due to the small -featureset this was also delisted from the list. It was there previously -before the approval process was introduced. From 0e98a080976f39e20aa6f18e8a335aa23d9334f7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 25 Jun 2011 12:26:07 +0200 Subject: [PATCH 0649/3747] Provide ways to override the url rule --- flask/app.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 1e068e41..c7b287a6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -200,6 +200,12 @@ class Flask(_PackageBoundObject): 'MAX_CONTENT_LENGTH': None }) + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + #: the test client that is used with when `test_client` is used. #: #: .. versionadded:: 0.7 @@ -573,7 +579,7 @@ class Flask(_PackageBoundObject): if 'OPTIONS' not in methods: methods = tuple(methods) + ('OPTIONS',) provide_automatic_options = True - rule = Rule(rule, methods=methods, **options) + rule = self.url_rule_class(rule, methods=methods, **options) rule.provide_automatic_options = provide_automatic_options self.url_map.add(rule) if view_func is not None: From 8e928a2fcbac6edca918e9d4efccc4fedb418292 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 25 Jun 2011 14:09:37 -0400 Subject: [PATCH 0650/3747] Document Werkzeug's run_simple for dispatch, #225. --- docs/deploying/index.rst | 2 ++ docs/patterns/appdispatch.rst | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index bd0d1626..d258df89 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -1,3 +1,5 @@ +.. _deployment: + Deployment Options ================== diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 2f8be093..bc498cf2 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -15,6 +15,22 @@ The fundamental difference from the :ref:`module approach different Flask applications that are entirely isolated from each other. They run different configurations and are dispatched on the WSGI level. + +Working with this Document +-------------------------- + +Each of the techniques and examples below results in an ``application`` object +that can be run with any WSGI server. For production, see :ref:`deployment`. +For development, Werkzeug provides a builtin server for development available +at :func:`werkzeug.serving.run_simple`:: + + from werkzeug.serving import run_simple + run_simple('localhost', 5000, application, use_reloader=True) + +Note that :func:`run_simple ` is not intended for +use in production. Use a :ref:`full-blown WSGI server `. + + Combining Applications ---------------------- From ab6bac111c9fa551ae0b6386a2cd6a13b45c26c7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 26 Jun 2011 21:34:27 +0200 Subject: [PATCH 0651/3747] Made it theoretically possible to hook into request matching --- flask/ctx.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flask/ctx.py b/flask/ctx.py index 1e700a84..3ea43b97 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -90,12 +90,7 @@ class RequestContext(object): self.flashes = None self.session = None - try: - url_rule, self.request.view_args = \ - self.url_adapter.match(return_rule=True) - self.request.url_rule = url_rule - except HTTPException, e: - self.request.routing_exception = e + self.match_request() # Support for deprecated functionality. This is doing away with # Flask 1.0 @@ -107,6 +102,17 @@ class RequestContext(object): if bp is not None and blueprint_is_module(bp): self.request._is_old_module = True + def match_request(self): + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + url_rule, self.request.view_args = \ + self.url_adapter.match(return_rule=True) + self.request.url_rule = url_rule + except HTTPException, e: + self.request.routing_exception = e + def push(self): """Binds the request context to the current context.""" _request_ctx_stack.push(self) From 3d146548c7e687b1938c66df12082a1ab037c75d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 00:31:24 +0200 Subject: [PATCH 0652/3747] Added release script --- CHANGES | 2 +- Makefile | 6 +- flask/__init__.py | 2 + scripts/make-release.py | 138 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 scripts/make-release.py diff --git a/CHANGES b/CHANGES index 3a367ead..00bc3f9a 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.7 ----------- -Release date to be announced, codename to be selected +Released on June 27th 2011, codename Grappa - Added :meth:`~flask.Flask.make_default_options_response` which can be used by subclasses to alter the default diff --git a/Makefile b/Makefile index a0127457..604c8a40 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,15 @@ test: audit: python setup.py audit +release: + python scripts/make-release.py + tox-test: PYTHONDONTWRITEBYTECODE= tox ext-test: python tests/flaskext_test.py --browse -release: - python setup.py release sdist upload - clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + diff --git a/flask/__init__.py b/flask/__init__.py index 03f91baa..19560123 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,6 +10,8 @@ :license: BSD, see LICENSE for more details. """ +__version__ = '0.7' + # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. from werkzeug import abort, redirect diff --git a/scripts/make-release.py b/scripts/make-release.py new file mode 100644 index 00000000..ceb13dc3 --- /dev/null +++ b/scripts/make-release.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + make-release + ~~~~~~~~~~~~ + + Helper script that performs a release. Does pretty much everything + automatically for us. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os +import re +from datetime import datetime, date +from subprocess import Popen, PIPE + + +def parse_changelog(): + with open('CHANGES') as f: + lineiter = iter(f) + for line in lineiter: + match = re.search('^Version\s+(.*)', line.strip()) + if match is None: + continue + length = len(match.group(1)) + version = match.group(1).strip() + if lineiter.next().count('-') != len(match.group(0)): + continue + while 1: + change_info = lineiter.next().strip() + if change_info: + break + + match = re.match(r'^released on (\w+\s+\d+\w+\s+\d+)' + r'(?:, codename (.*))?(?i)', change_info) + if match is None: + continue + + datestr, codename = match.groups() + return version, parse_date(datestr), codename + + +def parse_date(string): + string = string.replace('th ', ' ').replace('nd ', ' ') \ + .replace('rd ', ' ').replace('st ', ' ') + return datetime.strptime(string, '%B %d %Y') + + +def set_filename_version(filename, version_number, pattern): + changed = [] + def inject_version(match): + before, old, after = match.groups() + changed.append(True) + return before + version_number + after + with open(filename) as f: + contents = re.sub(r"^(\s*%s\s*=\s*')(.+?)(')(?sm)" % pattern, + inject_version, f.read()) + + if not changed: + fail('Could not find %s in %s', pattern, filename) + + with open(filename, 'w') as f: + f.write(contents) + + +def set_init_version(version): + info('Setting __init__.py version to %s', version) + set_filename_version('flask/__init__.py', version, '__version__') + + +def set_setup_version(version): + info('Setting setup.py version to %s', version) + set_filename_version('setup.py', version, 'version') + + +def build_and_upload(): + Popen([sys.executable, 'setup.py', 'sdist', 'release']).wait() + + +def fail(message, *args): + print >> sys.stderr, 'Error:', message % args + sys.exit(1) + + +def info(message, *args): + print >> sys.stderr, message % args + + +def get_git_tags(): + return set(Popen(['git', 'tag'], stdout=PIPE).communicate()[0].splitlines()) + + +def git_is_clean(): + return Popen(['git', 'diff', '--quiet']).wait() == 0 + + +def make_git_commit(message, *args): + message = message % args + Popen(['git', 'commit', '-am', message]).wait() + + +def make_git_tag(tag): + info('Tagging "%s"', tag) + Popen(['git', 'tag', tag]).wait() + + +def main(): + os.chdir(os.path.join(os.path.dirname(__file__), '..')) + + rv = parse_changelog() + if rv is None: + fail('Could not parse changelog') + + version, release_date, codename = rv + + info('Releasing %s (codename %s, release date %s)', + version, codename, release_date.strftime('%d/%m/%Y')) + tags = get_git_tags() + + if version in tags: + fail('Version "%s" is already tagged', version) + if release_date.date() != date.today(): + fail('Release date is not today (%s != %s)') + + if not git_is_clean(): + fail('You have uncommitted changes in git') + + set_init_version(version) + set_setup_version(version) + make_git_commit('Bump version number to %s', version) + make_git_tag(version) + build_and_upload() + + +if __name__ == '__main__': + main() From bffe97bb50c8d0b32d66abd59751fef51155a290 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 00:34:15 +0200 Subject: [PATCH 0653/3747] Added version bumping --- scripts/make-release.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/make-release.py b/scripts/make-release.py index ceb13dc3..349365c3 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -42,6 +42,15 @@ def parse_changelog(): return version, parse_date(datestr), codename +def bump_version(version): + try: + parts = map(int, version.split('.')) + except ValueError: + fail('Current version is not numeric') + parts[-1] += 1 + return '.'.join(map(str, parts)) + + def parse_date(string): string = string.replace('th ', ' ').replace('nd ', ' ') \ .replace('rd ', ' ').replace('st ', ' ') @@ -114,6 +123,7 @@ def main(): fail('Could not parse changelog') version, release_date, codename = rv + dev_version = bump_version(version) + '-dev' info('Releasing %s (codename %s, release date %s)', version, codename, release_date.strftime('%d/%m/%Y')) @@ -132,6 +142,8 @@ def main(): make_git_commit('Bump version number to %s', version) make_git_tag(version) build_and_upload() + set_init_version(dev_version) + set_setup_version(dev_version) if __name__ == '__main__': From 874079bffed0f02235f55e4a512822194ac2449a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 00:45:31 +0200 Subject: [PATCH 0654/3747] It's still dev :) --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 19560123..da75f460 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.7' +__version__ = '0.7-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index d2a9266b..22e00037 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def run_tests(): setup( name='Flask', - version='0.7', + version='0.7-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 85efa3f89571d66071b86110bf7b615e38234aa8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 01:04:18 +0200 Subject: [PATCH 0655/3747] Updated upgrade script to no longer change url_for that don't need to be changed --- scripts/flask-07-upgrade.py | 65 ++++++++++++++++++++------- scripts/testproj/templates/index.html | 1 + scripts/testproj/test.py | 11 ++++- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 7559f7a3..4027d8ce 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -46,7 +46,7 @@ _module_constructor_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\s*=\s*Module' r'\(__name__\s*(?:,\s*(?:name\s*=\s*)?(%s))?' % _string_re_part) _error_handler_re = re.compile(r'%s\.error_handlers\[\s*(\d+)\s*\]' % _app_re_part) -_mod_route_re = re.compile(r'([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') +_mod_route_re = re.compile(r'@([a-zA-Z0-9_][a-zA-Z0-9_]*)\.route') _blueprint_related = [ (re.compile(r'request\.module'), 'request.blueprint'), (re.compile(r'register_module'), 'register_blueprint'), @@ -62,18 +62,6 @@ def make_diff(filename, old, new): print line -def fix_url_for(contents): - def handle_match(match): - prefix = match.group(1) - endpoint = ast.literal_eval(match.group(2)) - if endpoint.startswith('.'): - endpoint = endpoint[1:] - else: - endpoint = '.' + endpoint - return prefix + repr(endpoint) - return _url_for_re.sub(handle_match, contents) - - def looks_like_teardown_function(node): returns = [x for x in ast.walk(node) if isinstance(x, ast.Return)] if len(returns) != 1: @@ -94,6 +82,48 @@ def looks_like_teardown_function(node): return resp_name.id +def fix_url_for(contents, module_declarations=None): + if module_declarations is None: + skip_module_test = True + else: + skip_module_test = False + mapping = dict(module_declarations) + annotated_lines = [] + + def make_line_annotations(): + if not annotated_lines: + last_index = 0 + for line in contents.splitlines(True): + last_index += len(line) + annotated_lines.append((last_index, line)) + + def backtrack_module_name(call_start): + make_line_annotations() + for idx, (line_end, line) in enumerate(annotated_lines): + if line_end > call_start: + for _, line in reversed(annotated_lines[:idx]): + match = _mod_route_re.search(line) + if match is not None: + shortname = match.group(1) + return mapping.get(shortname) + + def handle_match(match): + if not skip_module_test: + modname = backtrack_module_name(match.start()) + if modname is None: + return match.group(0) + prefix = match.group(1) + endpoint = ast.literal_eval(match.group(2)) + if endpoint.startswith('.'): + endpoint = endpoint[1:] + elif '.' not in endpoint: + endpoint = '.' + endpoint + else: + return match.group(0) + return prefix + repr(endpoint) + return _url_for_re.sub(handle_match, contents) + + def fix_teardown_funcs(contents): def is_return_line(line): @@ -194,21 +224,22 @@ def rewrite_for_blueprints(contents, filename): for pattern, replacement in _blueprint_related: new_contents = pattern.sub(replacement, new_contents) - return new_contents + return new_contents, dict(modules_declared) def upgrade_python_file(filename, contents, teardown): - new_contents = fix_url_for(contents) + new_contents = contents if teardown: new_contents = fix_teardown_funcs(new_contents) - new_contents = rewrite_for_blueprints(new_contents, filename) + new_contents, modules = rewrite_for_blueprints(new_contents, filename) + new_contents = fix_url_for(new_contents, modules) new_contents = _error_handler_re.sub('\\1.error_handler_spec[None][\\2]', new_contents) make_diff(filename, contents, new_contents) def upgrade_template_file(filename, contents): - new_contents = fix_url_for(contents) + new_contents = fix_url_for(contents, None) make_diff(filename, contents, new_contents) diff --git a/scripts/testproj/templates/index.html b/scripts/testproj/templates/index.html index 12392e26..42626af7 100644 --- a/scripts/testproj/templates/index.html +++ b/scripts/testproj/templates/index.html @@ -1 +1,2 @@ {{ url_for('static', filename='test.css') }} +{{ url_for('foo.static', filename='test.css') }} diff --git a/scripts/testproj/test.py b/scripts/testproj/test.py index e7b68eaf..8c0f16ae 100644 --- a/scripts/testproj/test.py +++ b/scripts/testproj/test.py @@ -1,4 +1,4 @@ -from flask import Flask, Module, render_template +from flask import Flask, Module, render_template, url_for mod = Module(__name__) @@ -12,8 +12,17 @@ def after_request(response): return response +@app.route('/') +def index_foo(): + x1 = url_for('somemod.index') + x2 = url_for('.index') + return render_template('test/index.html') + + @mod.route('/') def index(): + x1 = url_for('somemod.index') + x2 = url_for('.index') return render_template('test/index.html') From 12761bd02ce8480c228008792861c8ce357d396a Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sun, 26 Jun 2011 21:43:48 -0400 Subject: [PATCH 0656/3747] fixed pronoun gender in documentation --- docs/api.rst | 4 ++-- docs/patterns/errorpages.rst | 4 ++-- docs/patterns/fileuploads.rst | 6 +++--- docs/patterns/flashing.rst | 2 +- docs/patterns/viewdecorators.rst | 2 +- docs/quickstart.rst | 8 ++++---- docs/security.rst | 2 +- docs/tutorial/views.rst | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 88d026ed..109e0317 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -156,8 +156,8 @@ If you have the :attr:`Flask.secret_key` set you can use sessions in Flask applications. A session basically makes it possible to remember information from one request to another. The way Flask does this is by using a signed cookie. So the user can look at the session contents, but -not modify it unless he knows the secret key, so make sure to set that to -something complex and unguessable. +not modify it unless he or she knows the secret key, so make sure to set that +to something complex and unguessable. To access the current session you can use the :class:`session` object: diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index 4041bd8a..babeae4f 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -24,7 +24,7 @@ even if the application behaves correctly: *403 Forbidden* If you have some kind of access control on your website, you will have to send a 403 code for disallowed resources. So make sure the user - is not lost when he tries to access a resource he cannot access. + is not lost when he or she tries to access a forbidden resource. *410 Gone* Did you know that there the "404 Not Found" has a brother named "410 @@ -32,7 +32,7 @@ even if the application behaves correctly: resources that previously existed and got deleted answer with 410 instead of 404. If you are not deleting documents permanently from the database but just mark them as deleted, do the user a favour and - use the 410 code instead and display a message that what he was + use the 410 code instead and display a message that what he or she was looking for was deleted for all eternity. *500 Internal Server Error* diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 23d6c6bb..ab49cedb 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -89,7 +89,7 @@ before storing it directly on the filesystem. Assuming the number of ``../`` is correct and you would join this with the `UPLOAD_FOLDER` the user might have the ability to modify a file on - the server's filesystem he should not modify. This does require some + the server's filesystem he or she should not modify. This does require some knowledge about how the application looks like, but trust me, hackers are patient :) @@ -155,8 +155,8 @@ Upload Progress Bars A while ago many developers had the idea to read the incoming file in small chunks and store the upload progress in the database to be able to poll the progress with JavaScript from the client. Long story short: the -client asks the server every 5 seconds how much he has transmitted -already. Do you realize the irony? The client is asking for something he +client asks the server every 5 seconds how much it has transmitted +already. Do you realize the irony? The client is asking for something it should already know. Now there are better solutions to that work faster and more reliable. The diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 3610944e..a8291612 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -4,7 +4,7 @@ Message Flashing ================ Good applications and user interfaces are all about feedback. If the user -does not get enough feedback he will probably end up hating the +does not get enough feedback he or she will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index e99fc13e..9396fa91 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -8,7 +8,7 @@ functionality to one or more functions. The :meth:`~flask.Flask.route` decorator is the one you probably used already. But there are use cases for implementing your own decorator. For instance, imagine you have a view that should only be used by people that are logged in to. If a user -goes to the site and is not logged in, he should be redirected to the +goes to the site and is not logged in, he or she should be redirected to the login page. This is a good example of a use case where a decorator is an excellent solution. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 179ff2de..5c3f4411 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -126,7 +126,7 @@ Modern web applications have beautiful URLs. This helps people remember the URLs which is especially handy for applications that are used from mobile devices with slower network connections. If the user can directly go to the desired page without having to hit the index page it is more -likely he will like the page and come back next time. +likely he or she will like the page and come back next time. As you have seen above, the :meth:`~flask.Flask.route` decorator is used to bind a function to a URL. Here are some basic examples:: @@ -615,7 +615,7 @@ code use the :func:`~flask.abort` function. Here an example how this works:: this_is_never_executed() This is a rather pointless example because a user will be redirected from -the index to a page he cannot access (401 means access denied) but it +the index to a page he or she cannot access (401 means access denied) but it shows how that works. By default a black and white error page is shown for each error code. If @@ -642,7 +642,7 @@ Besides the request object there is also a second object called user from one request to the next. This is implemented on top of cookies for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, -unless he knows the secret key used for signing. +unless he or she knows the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: @@ -698,7 +698,7 @@ Message Flashing ---------------- Good applications and user interfaces are all about feedback. If the user -does not get enough feedback he will probably end up hating the +does not get enough feedback he or she will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next diff --git a/docs/security.rst b/docs/security.rst index 07a5b942..0e6ff67e 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -73,7 +73,7 @@ them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an attacker now creates a page that sends a post request to that page with -some JavaScript he just has to trick some users to load that page and +some JavaScript he or she just has to trick some users to load that page and their profiles will end up being deleted. Imagine you were to run Facebook with millions of concurrent users and diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index f2871257..466812dc 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -29,7 +29,7 @@ The view function will pass the entries as dicts to the Add New Entry ------------- -This view lets the user add new entries if he's logged in. This only +This view lets the user add new entries if he or she is logged in. This only responds to `POST` requests, the actual form is shown on the `show_entries` page. If everything worked out well we will :func:`~flask.flash` an information message to the next request and From 1c47d8a5f6f98e1dc40773f2b41dac688b440e9c Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 27 Jun 2011 08:21:09 +0200 Subject: [PATCH 0657/3747] Fix a link in the changelog. --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index e10744ca..abc923e1 100644 --- a/CHANGES +++ b/CHANGES @@ -40,7 +40,7 @@ Release date to be announced, codename to be selected - Added `teardown_request` decorator, for functions that should run at the end of a request regardless of whether an exception occurred. - Implemented :func:`flask.has_request_context` -- Added :func:`safe_join` +- Added :func:`flask.safe_join` - The automatic JSON request data unpacking now looks at the charset mimetype parameter. - Don't modify the session on :func:`flask.get_flashed_messages` if there From ea7a1720779ab4680304d2bc9dffcb6f5c7f2798 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 09:20:50 +0200 Subject: [PATCH 0658/3747] Test that dotted names work. This fixes #258 --- flask/wrappers.py | 2 +- tests/flask_tests.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 8db1ca9a..e592cc28 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -80,7 +80,7 @@ class Request(RequestBase): def blueprint(self): """The name of the current blueprint""" if self.url_rule and '.' in self.url_rule.endpoint: - return self.url_rule.endpoint.split('.', 1)[0] + return self.url_rule.endpoint.rsplit('.', 1)[0] @cached_property def json(self): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 9a0905c1..dae687dd 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1323,6 +1323,31 @@ class BlueprintTestCase(unittest.TestCase): with flask.Flask(__name__).test_request_context(): assert flask.render_template('nested/nested.txt') == 'I\'m nested' + def test_dotted_names(self): + frontend = flask.Blueprint('myapp.frontend', __name__) + backend = flask.Blueprint('myapp.backend', __name__) + + @frontend.route('/fe') + def frontend_index(): + return flask.url_for('myapp.backend.backend_index') + + @frontend.route('/fe2') + def frontend_page2(): + return flask.url_for('.frontend_index') + + @backend.route('/be') + def backend_index(): + return flask.url_for('myapp.frontend.frontend_index') + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + + c = app.test_client() + self.assertEqual(c.get('/fe').data.strip(), '/be') + self.assertEqual(c.get('/fe2').data.strip(), '/fe') + self.assertEqual(c.get('/be').data.strip(), '/fe') + class SendfileTestCase(unittest.TestCase): From 1e7577578f08f1685d98bdac5ce1c43973647aee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 09:30:27 +0200 Subject: [PATCH 0659/3747] Expanded url_for docstring for blueprints --- flask/helpers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 20cc8c77..0db7eb44 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -159,7 +159,13 @@ def url_for(endpoint, **values): Variable arguments that are unknown to the target endpoint are appended to the generated URL as query arguments. If the value of a query argument - is `None`, the whole pair is skipped. + is `None`, the whole pair is skipped. In case blueprints are active + you can shortcut references to the same blueprint by prefixing the + local endpoint with a dot (``.``). + + This will reference the index function local to the current blueprint:: + + url_for('.index') For more information, head over to the :ref:`Quickstart `. From ccd5ced70e48515e222b84dccc8f21d32c864dcd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 09:40:45 +0200 Subject: [PATCH 0660/3747] Chop of ports for session cookies. This fixes #253 --- flask/app.py | 3 ++- tests/flask_tests.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 55f79ebb..03a0c9f1 100644 --- a/flask/app.py +++ b/flask/app.py @@ -602,7 +602,8 @@ class Flask(_PackageBoundObject): if session.permanent: expires = datetime.utcnow() + self.permanent_session_lifetime if self.config['SERVER_NAME'] is not None: - domain = '.' + self.config['SERVER_NAME'] + # chop of the port which is usually not supported by browsers + domain = '.' + self.config['SERVER_NAME'].rsplit(':', 1)[0] session.save_cookie(response, self.session_cookie_name, expires=expires, httponly=True, domain=domain) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index dae687dd..ae43f93f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -306,6 +306,20 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() + def test_session_using_server_name_and_port(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + assert 'domain=.example.com' in rv.headers['set-cookie'].lower() + assert 'httponly' in rv.headers['set-cookie'].lower() + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): From 1c8097b35a08ef83d0996df0e51932ee3954e047 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 27 Jun 2011 09:43:51 +0200 Subject: [PATCH 0661/3747] Updated Python 3 section --- docs/foreword.rst | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 2c073218..616c298b 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -88,22 +88,17 @@ The Status of Python 3 ---------------------- Currently the Python community is in the process of improving libraries to -support the new iteration of the Python programming language. -Unfortunately there are a few problems with Python 3, namely the missing -consent on what WSGI for Python 3 should look like. These problems are +support the new iteration of the Python programming language. While the +situation is greatly improving there are still some issues that make it +hard for us to switch over to Python 3 just now. These problems are partially caused by changes in the language that went unreviewed for too -long, also partially the ambitions of everyone involved to drive the WSGI -standard forward. - -Because of that we strongly recommend against using Python 3 for web -development of any kind and wait until the WSGI situation is resolved. -You will find a couple of frameworks and web libraries on PyPI that claim -Python 3 support, but this support is based on the broken WSGI -implementation provided by Python 3.0 and 3.1 which will most likely -change in the near future. +long, partially also because we have not quite worked out how the lower +level API should change for the unicode differences in Python3. Werkzeug and Flask will be ported to Python 3 as soon as a solution for -WSGI is found, and we will provide helpful tips how to upgrade existing -applications to Python 3. Until then, we strongly recommend using Python -2.6 and 2.7 with activated Python 3 warnings during development, as well -as the Unicode literals `__future__` feature. +the changes is found, and we will provide helpful tips how to upgrade +existing applications to Python 3. Until then, we strongly recommend +using Python 2.6 and 2.7 with activated Python 3 warnings during +development. If you plan on upgrading to Python 3 in the near future we +strongly recommend that you read `How to write forwards compatible +Python code `_. From 7e6c70a133c833ef08f83a7e5cc926bf6e5581f1 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 27 Jun 2011 09:23:31 -0400 Subject: [PATCH 0662/3747] Add teardown request decorators to blueprints. --- flask/blueprints.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/flask/blueprints.py b/flask/blueprints.py index 4a3f531a..a0dfd521 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -216,6 +216,26 @@ class Blueprint(_PackageBoundObject): .setdefault(None, []).append(f)) return f + def teardown_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. This + function is only executed when tearing down requests handled by a + function of that blueprint. Teardown request functions are executed + when the request context is popped, even when no actual request was + performed. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def teardown_app_request(self, f): + """Like :meth:`Flask.teardown_request` but for a blueprint. Such a + function is executed when tearing down each request, even if outside of + the blueprint. + """ + self.record_once(lambda s: s.app.teardown_request_funcs + .setdefault(None, []).append(f)) + return f + def context_processor(self, f): """Like :meth:`Flask.context_processor` but for a blueprint. This function is only executed for requests handled by a blueprint. From dcf21989dc71f4ab93ceaf0627e40c516462bd8c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 12:45:49 +0200 Subject: [PATCH 0663/3747] Added class based views --- flask/app.py | 11 +++-- flask/views.py | 102 +++++++++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 37 ++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 flask/views.py diff --git a/flask/app.py b/flask/app.py index 03a0c9f1..eb7ba023 100644 --- a/flask/app.py +++ b/flask/app.py @@ -642,7 +642,8 @@ class Flask(_PackageBoundObject): if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ 'A blueprint\'s name collision ocurred between %r and ' \ - '%r. Both share the same name "%s"' % \ + '%r. Both share the same name "%s". Blueprints that ' \ + 'are created on the fly need unique names.' % \ (blueprint, self.blueprints[blueprint.name], blueprint.name) else: self.blueprints[blueprint.name] = blueprint @@ -695,7 +696,12 @@ class Flask(_PackageBoundObject): if endpoint is None: endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint - methods = options.pop('methods', ('GET',)) + methods = options.pop('methods', None) + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only `GET` as default. + if methods is None: + methods = getattr(view_func, 'methods', None) or ('GET',) provide_automatic_options = False if 'OPTIONS' not in methods: methods = tuple(methods) + ('OPTIONS',) @@ -778,7 +784,6 @@ class Flask(_PackageBoundObject): return f return decorator - def endpoint(self, endpoint): """A decorator to register a function as an endpoint. Example:: diff --git a/flask/views.py b/flask/views.py new file mode 100644 index 00000000..71f428f3 --- /dev/null +++ b/flask/views.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" + flask.views + ~~~~~~~~~~~ + + This module provides class based views inspired by the ones in Django. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from .globals import request + + +http_method_funcs = frozenset(['get', 'post', 'head', 'options', + 'delete', 'put', 'trace']) + + + +class View(object): + """Alternative way to use view functions. A subclass has to implement + :meth:`dispatch_request` which is called with the view arguments from + the URL routing system. If :attr:`methods` is provided the methods + do not have to be passed to the :meth:`~flask.Flask.add_url_rule` + method explicitly:: + + class MyView(View): + methods = ['GET'] + + def dispatch_request(self, name): + return 'Hello %s!' % name + + app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + """ + + methods = None + + def dispatch_request(self): + raise NotImplementedError() + + @classmethod + def as_view(cls, name, *class_args, **class_kwargs): + """Converts the class into an actual view function that can be + used with the routing system. What it does internally is generating + a function on the fly that will instanciate the :class:`View` + on each request and call the :meth:`dispatch_request` method on it. + + The arguments passed to :meth:`as_view` are forwarded to the + constructor of the class. + """ + def view(*args, **kwargs): + self = cls(*class_args, **class_kwargs) + return self.dispatch_request(*args, **kwargs) + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods + return view + + +class MethodViewType(type): + + def __new__(cls, name, bases, d): + rv = type.__new__(cls, name, bases, d) + if rv.methods is None: + methods = [] + for key, value in d.iteritems(): + if key in http_method_funcs: + methods.append(key.upper()) + # if we have no method at all in there we don't want to + # add a method list. (This is for instance the case for + # the baseclass or another subclass of a base method view + # that does not introduce new methods). + if methods: + rv.methods = methods + return rv + + +class MethodView(View): + """Like a regular class based view but that dispatches requests to + particular methods. For instance if you implement a method called + :meth:`get` it means you will response to ``'GET'`` requests and + the :meth:`dispatch_request` implementation will automatically + forward your request to that. Also :attr:`options` is set for you + automatically:: + + class CounterAPI(MethodView): + + def get(self): + return session.get('counter', 0) + + def post(self): + session['counter'] = session.get('counter', 0) + 1 + return 'OK' + + app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) + """ + __metaclass__ = MethodViewType + + def dispatch_request(self, *args, **kwargs): + meth = getattr(self, request.method.lower(), None) + assert meth is not None, 'Not implemented method' + return meth(*args, **kwargs) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index ae43f93f..80ccaffb 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -14,6 +14,7 @@ import os import re import sys import flask +import flask.views import unittest import warnings from threading import Thread @@ -23,6 +24,7 @@ from functools import update_wrapper from datetime import datetime from werkzeug import parse_date, parse_options_header from werkzeug.exceptions import NotFound +from werkzeug.http import parse_set_header from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -1753,6 +1755,40 @@ class TestSignals(unittest.TestCase): flask.got_request_exception.disconnect(record, app) +class ViewTestCase(unittest.TestCase): + + def common_test(self, app): + c = app.test_client() + + self.assertEqual(c.get('/').data, 'GET') + self.assertEqual(c.post('/').data, 'POST') + self.assertEqual(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assertEqual(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + class DeprecationsTestCase(unittest.TestCase): def test_init_jinja_globals(self): @@ -1785,6 +1821,7 @@ def suite(): suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(ConfigTestCase)) suite.addTest(unittest.makeSuite(SubdomainTestCase)) + suite.addTest(unittest.makeSuite(ViewTestCase)) suite.addTest(unittest.makeSuite(DeprecationsTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) From 775caf726db10c6c736a454b17fcc0f1f1474193 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:24:37 +0200 Subject: [PATCH 0664/3747] Improved cbv and added tests --- flask/views.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/flask/views.py b/flask/views.py index 71f428f3..9a185570 100644 --- a/flask/views.py +++ b/flask/views.py @@ -35,6 +35,10 @@ class View(object): methods = None def dispatch_request(self): + """Subclasses have to override this method to implement the + actual view functionc ode. This method is called with all + the arguments from the URL rule. + """ raise NotImplementedError() @classmethod @@ -48,8 +52,14 @@ class View(object): constructor of the class. """ def view(*args, **kwargs): - self = cls(*class_args, **class_kwargs) + self = view.view_class(*class_args, **class_kwargs) return self.dispatch_request(*args, **kwargs) + # we attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class based + # view this thing came from, secondly it's also used for instanciating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls view.__name__ = name view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ @@ -61,17 +71,17 @@ class MethodViewType(type): def __new__(cls, name, bases, d): rv = type.__new__(cls, name, bases, d) - if rv.methods is None: - methods = [] + if 'methods' not in d: + methods = set(rv.methods or []) for key, value in d.iteritems(): if key in http_method_funcs: - methods.append(key.upper()) + methods.add(key.upper()) # if we have no method at all in there we don't want to # add a method list. (This is for instance the case for # the baseclass or another subclass of a base method view # that does not introduce new methods). if methods: - rv.methods = methods + rv.methods = sorted(methods) return rv From b36d7b3288442173bcc999ebb4310674869a526d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:24:54 +0200 Subject: [PATCH 0665/3747] Added class based view documentation --- docs/api.rst | 13 ++++ docs/contents.rst.inc | 1 + docs/views.rst | 137 ++++++++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 41 +++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 docs/views.rst diff --git a/docs/api.rst b/docs/api.rst index ddc5a088..c2ea5b62 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -413,3 +413,16 @@ Signals operations, including connecting. .. _blinker: http://pypi.python.org/pypi/blinker + +Class Based Views +----------------- + +.. versionadded:: 0.7 + +.. currentmodule:: None + +.. autoclass:: flask.views.View + :members: + +.. autoclass:: flask.views.MethodView + :members: diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 6c49a878..7a8ebb12 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,6 +17,7 @@ instructions for web development with Flask. errorhandling config signals + views reqcontext blueprints shell diff --git a/docs/views.rst b/docs/views.rst new file mode 100644 index 00000000..a5b960e7 --- /dev/null +++ b/docs/views.rst @@ -0,0 +1,137 @@ +.. _views: + +Pluggable Views +=============== + +.. versionadded:: 0.7 + +Flask 0.7 introduces pluggable views inspired by the generic views from +Django which are based on classes instead of functions. The main +intention is that you can replace parts of the implementations and this +way have customizable pluggable views. + +Basic Principle +--------------- + +Consider you have a function that loads a list of objects from the +database and renders into a template:: + + @app.route('/users/') + def show_users(page): + users = User.query.all() + return render_template('users.html', users=users) + +This is simple and flexible, but if you want to provide this view in a +generic fashion that can be adapted to other models and templates as well +you might want more flexibility. This is where pluggable class based +views come into place. As the first step to convert this into a class +based view you would do this:: + + + from flask.views import View + + class ShowUsers(View): + + def dispatch_request(self): + users = User.query.all() + return render_template('users.html', objects=users) + + app.add_url_rule('/users/', ShowUsers.as_view('show_users')) + +As you can see what you have to do is to create a subclass of +:class:`flask.views.View` and implement +:meth:`~flask.views.View.dispatch_request`. Then we have to convert that +class into an actual view function by using the +:meth:`~flask.views.View.as_view` class method. The string you pass to +that function is the name of the endpoint that view will then have. But +this by itself is not helpful, so let's refactor the code a bit:: + + + from flask.views import View + + class ListView(View): + + def get_template_name(self): + raise NotImplementedError() + + def render_template(self, context): + return render_template(self.get_template_name(), **context) + + def dispatch_request(self): + context = {'objects': self.get_objects()} + return self.render_template(context) + + class UserView(ListView): + + def get_template_name(self): + return 'users.html' + + def get_objects(self): + return User.query.all() + +This of course is not that helpful for such a small example, but it's good +enough to explain the basic principle. When you have a class based view +the question comes up what `self` points to. The way this works is that +whenever the request is dispatched a new instance of the class is created +and the :meth:`~flask.views.View.dispatch_request` method is called with +the parameters from the URL rule. The class itself is instanciated with +the parameters passed to the :meth:`~flask.views.View.as_view` function. +For instance you can write a class like this:: + + class RenderTemplateView(View): + def __init__(self, template_name): + self.template_name = template_name + def dispatch_request(self): + return render_template(self.template_name) + +And then you can register it like this:: + + app.add_url_view('/about', RenderTemplateView.as_view( + 'about_page', template_name='about.html')) + +Method Hints +------------ + +Pluggable views are attached to the application like a regular function by +either using :func:`~flask.Flask.route` or better +:meth:`~flask.Flask.add_url_rule`. That however also means that you would +have to provide the names of the HTTP methods the view supports when you +attach this. In order to move that information to the class you can +provide a :attr:`~flask.views.View.methods` attribute that has this +information:: + + class MyView(View): + methods = ['GET', 'POST'] + + def dispatch_request(self): + if request.method == 'POST': + ... + ... + + app.add_url_view('/myview', MyView.as_view('myview')) + +Method Based Dispatching +------------------------ + +For RESTful APIs it's especially helpful to execute a different function +for each HTTP method. With the :class:`flask.views.MethodView` you can +easily do that. Each HTTP method maps to a function with the same name +(just in lowercase):: + + from flask.views import MethodView + + class UserAPI(MethodView): + + def get(self): + users = User.query.all() + ... + + def post(self): + user = User.from_form_data(request.form) + ... + + app.add_url_view('/users/', UserAPI.as_view('users')) + +That way you also don't have to provide the +:attr:`~flask.views.View.methods` attribute. It's automatically set based +on the methods defined in the class. diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 80ccaffb..e04c2458 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1787,8 +1787,49 @@ class ViewTestCase(unittest.TestCase): return 'POST' app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + class DeprecationsTestCase(unittest.TestCase): def test_init_jinja_globals(self): From 81433cec2987036b3c04d7f5211b128355235837 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:26:12 +0200 Subject: [PATCH 0666/3747] Added changelog entry --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 00bc3f9a..78d3d58d 100644 --- a/CHANGES +++ b/CHANGES @@ -56,6 +56,7 @@ Released on June 27th 2011, codename Grappa might occur during request processing (for instance database connection errors, timeouts from remote resources etc.). - Blueprints can provide blueprint specific error handlers. +- Implemented generic pluggable views (class based views). Version 0.6.1 ------------- From 22d468e190c4cdd3af8ea0546157f22b53197855 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:34:07 +0200 Subject: [PATCH 0667/3747] Less annoying gender neutral forms --- docs/api.rst | 2 +- docs/patterns/errorpages.rst | 4 ++-- docs/patterns/flashing.rst | 2 +- docs/patterns/viewdecorators.rst | 2 +- docs/quickstart.rst | 8 ++++---- docs/security.rst | 2 +- docs/tutorial/introduction.rst | 2 +- docs/tutorial/views.rst | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c2ea5b62..b21ec2ce 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -156,7 +156,7 @@ If you have the :attr:`Flask.secret_key` set you can use sessions in Flask applications. A session basically makes it possible to remember information from one request to another. The way Flask does this is by using a signed cookie. So the user can look at the session contents, but -not modify it unless he or she knows the secret key, so make sure to set that +not modify it unless they know the secret key, so make sure to set that to something complex and unguessable. To access the current session you can use the :class:`session` object: diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index babeae4f..ddf73c93 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -24,7 +24,7 @@ even if the application behaves correctly: *403 Forbidden* If you have some kind of access control on your website, you will have to send a 403 code for disallowed resources. So make sure the user - is not lost when he or she tries to access a forbidden resource. + is not lost when they try to access a forbidden resource. *410 Gone* Did you know that there the "404 Not Found" has a brother named "410 @@ -32,7 +32,7 @@ even if the application behaves correctly: resources that previously existed and got deleted answer with 410 instead of 404. If you are not deleting documents permanently from the database but just mark them as deleted, do the user a favour and - use the 410 code instead and display a message that what he or she was + use the 410 code instead and display a message that what they were looking for was deleted for all eternity. *500 Internal Server Error* diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index a8291612..7abe7165 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -4,7 +4,7 @@ Message Flashing ================ Good applications and user interfaces are all about feedback. If the user -does not get enough feedback he or she will probably end up hating the +does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 9396fa91..a0948577 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -8,7 +8,7 @@ functionality to one or more functions. The :meth:`~flask.Flask.route` decorator is the one you probably used already. But there are use cases for implementing your own decorator. For instance, imagine you have a view that should only be used by people that are logged in to. If a user -goes to the site and is not logged in, he or she should be redirected to the +goes to the site and is not logged in, they should be redirected to the login page. This is a good example of a use case where a decorator is an excellent solution. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b6fdecd0..5eaaf9c5 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -126,7 +126,7 @@ Modern web applications have beautiful URLs. This helps people remember the URLs which is especially handy for applications that are used from mobile devices with slower network connections. If the user can directly go to the desired page without having to hit the index page it is more -likely he or she will like the page and come back next time. +likely they will like the page and come back next time. As you have seen above, the :meth:`~flask.Flask.route` decorator is used to bind a function to a URL. Here are some basic examples:: @@ -614,7 +614,7 @@ code use the :func:`~flask.abort` function. Here an example how this works:: this_is_never_executed() This is a rather pointless example because a user will be redirected from -the index to a page he or she cannot access (401 means access denied) but it +the index to a page they cannot access (401 means access denied) but it shows how that works. By default a black and white error page is shown for each error code. If @@ -641,7 +641,7 @@ Besides the request object there is also a second object called user from one request to the next. This is implemented on top of cookies for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, -unless he or she knows the secret key used for signing. +unless they know the secret key used for signing. In order to use sessions you have to set a secret key. Here is how sessions work:: @@ -697,7 +697,7 @@ Message Flashing ---------------- Good applications and user interfaces are all about feedback. If the user -does not get enough feedback he or she will probably end up hating the +does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next diff --git a/docs/security.rst b/docs/security.rst index 0e6ff67e..909ef537 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -73,7 +73,7 @@ them knowing. Say you have a specific URL that, when you sent `POST` requests to will delete a user's profile (say `http://example.com/user/delete`). If an attacker now creates a page that sends a post request to that page with -some JavaScript he or she just has to trick some users to load that page and +some JavaScript they just has to trick some users to load that page and their profiles will end up being deleted. Imagine you were to run Facebook with millions of concurrent users and diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index 00fe3a00..c72bbd7d 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -8,7 +8,7 @@ less web-2.0-ish name ;) Basically we want it to do the following things: 1. let the user sign in and out with credentials specified in the configuration. Only one user is supported. -2. when the user is logged in he or she can add new entries to the page +2. when the user is logged in they can add new entries to the page consisting of a text-only title and some HTML for the text. This HTML is not sanitized because we trust the user here. 3. the page shows all entries so far in reverse order (newest on top) and diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 466812dc..93bec3bf 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -29,7 +29,7 @@ The view function will pass the entries as dicts to the Add New Entry ------------- -This view lets the user add new entries if he or she is logged in. This only +This view lets the user add new entries if they are logged in. This only responds to `POST` requests, the actual form is shown on the `show_entries` page. If everything worked out well we will :func:`~flask.flash` an information message to the next request and From 97c54fd484221cb53e91de9b35f7c132970aa045 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:36:49 +0200 Subject: [PATCH 0668/3747] Correct release date --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index d4a19d08..b9302834 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.7 ----------- -Released on June 27th 2011, codename Grappa +Released on June 28th 2011, codename Grappa - Added :meth:`~flask.Flask.make_default_options_response` which can be used by subclasses to alter the default From fb1482d3bb1b95803d25247479eb8ca8317a3219 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:36:52 +0200 Subject: [PATCH 0669/3747] Bump version number to 0.7 --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index da75f460..19560123 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.7-dev' +__version__ = '0.7' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 22e00037..d2a9266b 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def run_tests(): setup( name='Flask', - version='0.7-dev', + version='0.7', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From b64fa73467424b8e6c4efa565d4d5bc2ae414ab7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:37:02 +0200 Subject: [PATCH 0670/3747] HEAD is 0.8-dev --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 19560123..1c5e3ba2 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.7' +__version__ = '0.8-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index d2a9266b..1809e05e 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def run_tests(): setup( name='Flask', - version='0.7', + version='0.8-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 62e7f5cea62c5fc735c1993c3c11a29825b71966 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 14:45:11 +0200 Subject: [PATCH 0671/3747] Better uploads --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 604c8a40..7b14422f 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,9 @@ upload-docs: $(MAKE) -C docs html dirhtml latex $(MAKE) -C docs/_build/latex all-pdf cd docs/_build/; mv html flask-docs; zip -r flask-docs.zip flask-docs; mv flask-docs html - scp -r docs/_build/dirhtml/* pocoo.org:/var/www/flask.pocoo.org/docs/ - scp -r docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf - scp -r docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/ + rsync -a docs/_build/dirhtml/ pocoo.org:/var/www/flask.pocoo.org/docs/ + rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf + rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip docs: $(MAKE) -C docs html From 0ff7fe2a378d89308e3e6b2740ac1469157fa088 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 28 Jun 2011 06:14:39 -0700 Subject: [PATCH 0672/3747] Typo fix in code samples for Pluggable Views doc. --- docs/views.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/views.rst b/docs/views.rst index a5b960e7..fc22d1d7 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -86,7 +86,7 @@ For instance you can write a class like this:: And then you can register it like this:: - app.add_url_view('/about', RenderTemplateView.as_view( + app.add_url_rule('/about', view_func=RenderTemplateView.as_view( 'about_page', template_name='about.html')) Method Hints @@ -108,7 +108,7 @@ information:: ... ... - app.add_url_view('/myview', MyView.as_view('myview')) + app.add_url_rule('/myview', view_func=MyView.as_view('myview')) Method Based Dispatching ------------------------ @@ -130,7 +130,7 @@ easily do that. Each HTTP method maps to a function with the same name user = User.from_form_data(request.form) ... - app.add_url_view('/users/', UserAPI.as_view('users')) + app.add_url_rule('/users/', view_func=UserAPI.as_view('users')) That way you also don't have to provide the :attr:`~flask.views.View.methods` attribute. It's automatically set based From ee001e192d9178ff43c0511dc4292299b0a534d3 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 28 Jun 2011 06:25:52 -0700 Subject: [PATCH 0673/3747] Add a link to pluggable views docs in the changelog. --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index b9302834..e02a393b 100644 --- a/CHANGES +++ b/CHANGES @@ -56,7 +56,7 @@ Released on June 28th 2011, codename Grappa might occur during request processing (for instance database connection errors, timeouts from remote resources etc.). - Blueprints can provide blueprint specific error handlers. -- Implemented generic pluggable views (class based views). +- Implemented generic :ref:`views` (class based views). Version 0.6.1 ------------- From 5afeac0c19b6c9ca6456dff368724492822ea165 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 16:06:51 +0200 Subject: [PATCH 0674/3747] Fixed release script :( --- scripts/make-release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/make-release.py b/scripts/make-release.py index 349365c3..9955b13e 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -85,7 +85,7 @@ def set_setup_version(version): def build_and_upload(): - Popen([sys.executable, 'setup.py', 'sdist', 'release']).wait() + Popen([sys.executable, 'setup.py', 'release', 'sdist', 'upload']).wait() def fail(message, *args): From d0821edca5a075383a98d88d2916b5991897421b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 16:08:19 +0200 Subject: [PATCH 0675/3747] Fixed link to the upgrade script. github changed links apparently --- docs/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7fcef042..d5b05923 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -45,7 +45,7 @@ good. To apply the upgrade script do the following: 1. Download the script: `flask-07-upgrade.py - `_ + `_ 2. Run it in the directory of your application:: python flask-07-upgrade.py > patchfile.diff From e2fed6c3a771514a0f6ed2d10e239ef901305713 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 16:11:01 +0200 Subject: [PATCH 0676/3747] Fixed another typo --- docs/blueprints.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 175ce69f..b9fa0e4a 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -22,7 +22,7 @@ Blueprints in Flask are intended for these cases: larger applications; a project could instantiate an application object, initialize several extensions, and register a collection of blueprints. * Register a blueprint on an application at a URL prefix and/or subdomain. - Paremeters in the URL prefix/subdomain become common view arguments + Parameters in the URL prefix/subdomain become common view arguments (with defaults) across all view functions in the blueprint. * Register a blueprint multiple times on an application with different URL rules. From 57d9a2a1187b306c6997a3dcbac25837a54862f2 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 28 Jun 2011 12:00:01 -0400 Subject: [PATCH 0677/3747] Import with statement in helpers.py, #264. --- flask/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index 0db7eb44..2a841236 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os import sys import posixpath From 5aabc4070096b6e9dca56b62b8afb514d641f5cd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 23:36:39 +0200 Subject: [PATCH 0678/3747] Added changelog entry Signed-off-by: Armin Ronacher --- CHANGES | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES b/CHANGES index e02a393b..1e559167 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,13 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.7.1 +------------- + +Bugfix release, release date to be decided. + +- Added missing future import that broke 2.5 compatibility. + Version 0.7 ----------- From 7d4c7847085dfe19893ff2a7d4c0d1bd0fbb7981 Mon Sep 17 00:00:00 2001 From: Luit van Drongelen Date: Wed, 29 Jun 2011 01:06:03 -0700 Subject: [PATCH 0679/3747] Missed the -w option in uWSGI deployment docs. --- docs/deploying/uwsgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index 6f373731..bdee15ba 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -36,7 +36,7 @@ Or, if you prefer: .. sourcecode:: text - $ uwsgi -s /tmp/uwsgi.sock myapp:app + $ uwsgi -s /tmp/uwsgi.sock -w myapp:app Configuring nginx ----------------- From c26a6dd50c847d083cf444616eb5a8c99fb8b43b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 29 Jun 2011 15:36:02 +0200 Subject: [PATCH 0680/3747] Admin -> Backend --- docs/patterns/appdispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index bc498cf2..129fce7a 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -42,7 +42,7 @@ are combined by the dispatcher middleware into a larger one that dispatched based on prefix. For example you could have your main application run on `/` and your -backend interface on `/admin`:: +backend interface on `/backend`:: from werkzeug.wsgi import DispatcherMiddleware from frontend_app import application as frontend From a101cfc35be437ad0e4f56da0d92f14c27abda67 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 29 Jun 2011 18:31:48 +0200 Subject: [PATCH 0681/3747] Worked around a werkzeug bug with redirects --- CHANGES | 1 + flask/app.py | 6 ++++++ tests/flask_tests.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGES b/CHANGES index 1e559167..e611c4ab 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,7 @@ Version 0.7.1 Bugfix release, release date to be decided. - Added missing future import that broke 2.5 compatibility. +- Fixed an infinite redirect issue with blueprints. Version 0.7 ----------- diff --git a/flask/app.py b/flask/app.py index eb7ba023..dff4272b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -706,6 +706,12 @@ class Flask(_PackageBoundObject): if 'OPTIONS' not in methods: methods = tuple(methods) + ('OPTIONS',) provide_automatic_options = True + + # due to a werkzeug bug we need to make sure that the defaults are + # None if they are an empty dictionary. This should not be necessary + # with Werkzeug 0.7 + options['defaults'] = options.get('defaults') or None + rule = self.url_rule_class(rule, methods=methods, **options) rule.provide_automatic_options = provide_automatic_options self.url_map.add(rule) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e04c2458..fc99cd16 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1364,6 +1364,21 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/fe2').data.strip(), '/fe') self.assertEqual(c.get('/be').data.strip(), '/fe') + def test_empty_url_defaults(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/', defaults={'page': 1}) + @bp.route('/page/') + def something(page): + return str(page) + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + self.assertEqual(c.get('/').data, '1') + self.assertEqual(c.get('/page/2').data, '2') + class SendfileTestCase(unittest.TestCase): From 510823c1592abe4c57e051e9d21bd134ab7f07a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 28 Jun 2011 23:37:03 +0200 Subject: [PATCH 0682/3747] Bumped version number for this branch to 0.7.1 --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 1c5e3ba2..e675eacf 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.8-dev' +__version__ = '0.7.1-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 1809e05e..7d2d4158 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def run_tests(): setup( name='Flask', - version='0.8-dev', + version='0.7.1-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 21d4a054e399a42ac448814116580a5c62991fce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 29 Jun 2011 18:35:11 +0200 Subject: [PATCH 0683/3747] Fixed a bug in the release script Signed-off-by: Armin Ronacher --- scripts/make-release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/make-release.py b/scripts/make-release.py index 9955b13e..301e4d6d 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -33,7 +33,7 @@ def parse_changelog(): if change_info: break - match = re.match(r'^released on (\w+\s+\d+\w+\s+\d+)' + match = re.match(r'released on (\w+\s+\d+\w+\s+\d+)' r'(?:, codename (.*))?(?i)', change_info) if match is None: continue From 7e1ebae3a2879b989cae4d27bb8dd1dc8a1313ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 29 Jun 2011 18:37:08 +0200 Subject: [PATCH 0684/3747] Another fix for the script Signed-off-by: Armin Ronacher --- scripts/make-release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/make-release.py b/scripts/make-release.py index 301e4d6d..574cb75d 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -33,8 +33,8 @@ def parse_changelog(): if change_info: break - match = re.match(r'released on (\w+\s+\d+\w+\s+\d+)' - r'(?:, codename (.*))?(?i)', change_info) + match = re.search(r'released on (\w+\s+\d+\w+\s+\d+)' + r'(?:, codename (.*))?(?i)', change_info) if match is None: continue From c6dbcd572b4d207701c44caea71097d198089a16 Mon Sep 17 00:00:00 2001 From: Hadley Rich Date: Thu, 30 Jun 2011 22:44:28 +1200 Subject: [PATCH 0685/3747] Trivial fix for PathDispatcher example usage --- docs/patterns/appdispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 129fce7a..93b4af96 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -167,4 +167,4 @@ falls back to another application if the creator function returns `None`:: if user is not None: return create_app(user) - application = PathDispatcher('example.com', default_app, make_app) + application = PathDispatcher(default_app, make_app) From 8ba6673670ec58e9cf32c6525d1903bc8f9b66d9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 1 Jul 2011 00:50:18 +0200 Subject: [PATCH 0686/3747] Fixed a changelog entry --- docs/upgrading.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index d5b05923..1d91596d 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -193,10 +193,11 @@ to upgrade. What changed? location however. If you want to continue serving static files you need to tell the constructor explicitly the path to the static folder (which can be relative to the blueprint's module path). -- Rendering templates was simplified. Now the general syntax is - ``blueprint-shortname:template-name`` for rendering templates instead - of ``blueprint-shortname/template-name`` which was confusing and often - clashed with templates from the global template loader. +- Rendering templates was simplified. Now the blueprints can provide + template folders which are added to a general template searchpath. + This means that you need to add another subfolder with the blueprint's + name into that folder if you want ``blueprintname/template.html`` as + the template name. If you continue to use the `Module` object which is deprecated, Flask will restore the previous behavior as good as possible. However we strongly From 0fb2e4c84859d9d31fe2170df3f9d51e36a3f3c0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 1 Jul 2011 00:52:06 +0200 Subject: [PATCH 0687/3747] More invalid doc fixing --- docs/upgrading.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 1d91596d..df084e51 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -189,10 +189,12 @@ to upgrade. What changed? some unnecessary leading dots in your code if you're not using modules. - Blueprints do not automatically provide static folders. They will - still export templates from a folder called `templates` next to their - location however. If you want to continue serving static files you - need to tell the constructor explicitly the path to the static folder - (which can be relative to the blueprint's module path). + also no longer automatically export templates from a folder called + `templates` next to their location however but it can be enabled from + the constructor. Same with static files: if you want to continue + serving static files you need to tell the constructor explicitly the + path to the static folder (which can be relative to the blueprint's + module path). - Rendering templates was simplified. Now the blueprints can provide template folders which are added to a general template searchpath. This means that you need to add another subfolder with the blueprint's From 15372661af2974577d5c1d15fdaf88452661c2a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 1 Jul 2011 00:56:06 +0200 Subject: [PATCH 0688/3747] Removed leftover from no longer supported blueprint template loading code. --- flask/templating.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index d50691b3..147f49d0 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -117,8 +117,6 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - if template_name[:1] == ':': - template_name = ctx.request.blueprint + template_name return _render(ctx.app.jinja_env.get_template(template_name), context, ctx.app) From bd473c158788bd2c1153fc8fe45831aaa648824a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 Jul 2011 10:16:56 +0200 Subject: [PATCH 0689/3747] Fixed an issue that broke url processors for blueprints. Added testcases --- flask/blueprints.py | 4 +-- tests/flask_tests.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index a0dfd521..0c3a7d0f 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -56,7 +56,7 @@ class BlueprintSetupState(object): #: A dictionary with URL defaults that is added to each and every #: URL that was defined with the blueprint. - self.url_defaults = dict(self.blueprint.url_defaults) + self.url_defaults = dict(self.blueprint.url_values_defaults) self.url_defaults.update(self.options.get('url_defaults', ())) def add_url_rule(self, rule, endpoint=None, view_func=None, **options): @@ -102,7 +102,7 @@ class Blueprint(_PackageBoundObject): self.view_functions = {} if url_defaults is None: url_defaults = {} - self.url_defaults = url_defaults + self.url_values_defaults = url_defaults def record(self, func): """Registers a function that is called when the blueprint is diff --git a/tests/flask_tests.py b/tests/flask_tests.py index fc99cd16..e13dfd36 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -830,6 +830,37 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = c.post('/accept', data={'myfile': 'foo' * 100}) assert rv.data == '42' + def test_url_processors(self): + app = flask.Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and \ + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values.setdefault('lang_code', flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + return flask.url_for('about') + + @app.route('//about') + def about(): + return flask.url_for('something_else') + + @app.route('/foo') + def something_else(): + return flask.url_for('about', lang_code='en') + + c = app.test_client() + + self.assertEqual(c.get('/de/').data, '/de/about') + self.assertEqual(c.get('/de/about').data, '/foo') + self.assertEqual(c.get('/foo').data, '/en/about') + class JSONTestCase(unittest.TestCase): @@ -1309,6 +1340,33 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/1/bar').data, u'23') self.assertEqual(c.get('/2/bar').data, u'19') + def test_blueprint_url_processors(self): + bp = flask.Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + return flask.url_for('.about') + + @bp.route('/about') + def about(): + return flask.url_for('.index') + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + + self.assertEqual(c.get('/de/').data, '/de/about') + self.assertEqual(c.get('/de/about').data, '/de/') + def test_templates_and_static(self): from blueprintapp import app c = app.test_client() From 343d8a94e7ecd1c937685592482de71dcc7b6888 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 Jul 2011 10:18:03 +0200 Subject: [PATCH 0690/3747] Added changelog entry Signed-off-by: Armin Ronacher --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index e611c4ab..640e0d9b 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,14 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.7.2 +------------- + +Bugfix release, released on July 6th 2011 + +- Fixed an issue with URL processors not properly working on + blueprints. + Version 0.7.1 ------------- From 515e946b020584db7f236eb8ad139f9f32de305f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 Jul 2011 10:21:23 +0200 Subject: [PATCH 0691/3747] master is 0.8-dev --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index e675eacf..1c5e3ba2 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.7.1-dev' +__version__ = '0.8-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 7d2d4158..1809e05e 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ def run_tests(): setup( name='Flask', - version='0.7.1-dev', + version='0.8-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 1c05f47c3b7c6674aadfe5636078ffa1aa81d7a1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 Jul 2011 10:26:59 +0200 Subject: [PATCH 0692/3747] Forgot to cherry-pick the release date for 0.7.1 in master --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 640e0d9b..b414f5ad 100644 --- a/CHANGES +++ b/CHANGES @@ -14,7 +14,7 @@ Bugfix release, released on July 6th 2011 Version 0.7.1 ------------- -Bugfix release, release date to be decided. +Bugfix release, released on June 29th 2011 - Added missing future import that broke 2.5 compatibility. - Fixed an infinite redirect issue with blueprints. From de3b6ab5ee6f14d8e280b7aa0f9193b265b3d6dc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 6 Jul 2011 11:33:02 +0200 Subject: [PATCH 0693/3747] Fixed a broken sentence about blueprints. --- docs/blueprints.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index b9fa0e4a..c71d11bb 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -41,7 +41,8 @@ will have separate configs and will be managed at the WSGI layer. Blueprints instead provide separation at the Flask level, share application config, and can change an application object as necessary with being registered. The downside is that you cannot unregister a blueprint -once application without having to destroy the whole application object. +once an application was created without having to destroy the whole +application object. The Concept of Blueprints ------------------------- From d63fb818c14ea6acb36942cd66f5ed00dfa3682b Mon Sep 17 00:00:00 2001 From: Bastian Hoyer Date: Wed, 6 Jul 2011 16:07:05 +0200 Subject: [PATCH 0694/3747] small error in tutorial --- docs/tutorial/dbcon.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 1d9d41f9..8f9e4595 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -22,7 +22,7 @@ decorators:: Functions marked with :meth:`~flask.Flask.before_request` are called before a request and passed no arguments. Functions marked with -:meth:`~flask.Flask.teardown_request` are called after a request and +:meth:`~flask.Flask.after_request` are called after a request and passed the response that will be sent to the client. They have to return that response object or a different one. They are however not guaranteed to be executed if an exception is raised, this is where functions marked with From 1a61b12dbf8909ec7dd2f630b285338a8547e912 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 11:26:53 +0200 Subject: [PATCH 0695/3747] Added a row for Flask 0.8 to the changelog --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index b414f5ad..f4684b56 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.8 +----------- + +Relase date to be decided, codename to be chosen. + Version 0.7.2 ------------- From 0fccfe711fda524f216f82263bb83787a75035fd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 11:27:22 +0200 Subject: [PATCH 0696/3747] Refactored session interface --- CHANGES | 4 ++++ docs/api.rst | 25 +++++++++++++++++++++++++ flask/__init__.py | 1 - flask/app.py | 38 ++++++++++++++++++++++---------------- flask/ctx.py | 2 +- flask/session.py | 38 +++++++------------------------------- 6 files changed, 59 insertions(+), 49 deletions(-) diff --git a/CHANGES b/CHANGES index f4684b56..0db9003e 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.8 Relase date to be decided, codename to be chosen. +- Refactored session support into a session interface so that + the implementation of the sessions can be changed without + having to override the Flask class. + Version 0.7.2 ------------- diff --git a/docs/api.rst b/docs/api.rst index b21ec2ce..99071f87 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -195,9 +195,34 @@ To access the current session you can use the :class:`session` object: session will be deleted when the user closes the browser. +Session Interface +----------------- + +.. versionadded:: 0.7 + +The session interface provides a simple way to replace the session +implementation that Flask is using. + +.. currentmodule:: flask.sessions + +.. autoclass:: SessionInterface + :members: + +.. autoclass:: SecureCookieSessionInterface + :members: + +.. autoclass:: NullSession + :members: + +.. autoclass:: SessionMixin + :members: + + Application Globals ------------------- +.. currentmodule:: flask + To share data that is valid for one request only from one function to another, a global variable is not good enough because it would break in threaded environments. Flask provides you with a special object that diff --git a/flask/__init__.py b/flask/__init__.py index 1c5e3ba2..930859da 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -27,7 +27,6 @@ from .ctx import has_request_context from .module import Module from .blueprints import Blueprint from .templating import render_template, render_template_string -from .session import Session # the signals from .signals import signals_available, template_rendered, request_started, \ diff --git a/flask/app.py b/flask/app.py index dff4272b..162ce4f2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -27,7 +27,7 @@ from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import RequestContext from .globals import _request_ctx_stack, request -from .session import Session, _NullSession +from .sessions import SecureCookieSessionInterface from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor @@ -211,6 +211,12 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 test_client_class = None + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.7 + session_interface = SecureCookieSessionInterface() + def __init__(self, import_name, static_path=None, static_url_path=None, static_folder='static', template_folder='templates'): _PackageBoundObject.__init__(self, import_name, @@ -580,32 +586,32 @@ class Flask(_PackageBoundObject): def open_session(self, request): """Creates or opens a new session. Default implementation stores all session data in a signed cookie. This requires that the - :attr:`secret_key` is set. + :attr:`secret_key` is set. Instead of overriding this method + we recommend replacing the :class:`session_interface`. :param request: an instance of :attr:`request_class`. """ - key = self.secret_key - if key is not None: - return Session.load_cookie(request, self.session_cookie_name, - secret_key=key) + return self.session_interface.open_session(self, request) def save_session(self, session, response): """Saves the session if it needs updates. For the default - implementation, check :meth:`open_session`. + implementation, check :meth:`open_session`. Instead of overriding this + method we recommend replacing the :class:`session_interface`. :param session: the session to be saved (a :class:`~werkzeug.contrib.securecookie.SecureCookie` object) :param response: an instance of :attr:`response_class` """ - expires = domain = None - if session.permanent: - expires = datetime.utcnow() + self.permanent_session_lifetime - if self.config['SERVER_NAME'] is not None: - # chop of the port which is usually not supported by browsers - domain = '.' + self.config['SERVER_NAME'].rsplit(':', 1)[0] - session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True, domain=domain) + return self.session_interface.save_session(self, session, response) + + def make_null_session(self): + """Creates a new instance of a missing session. Instead of overriding + this method we recommend replacing the :class:`session_interface`. + + .. versionadded:: 0.7 + """ + return self.session_interface.make_null_session(self) def register_module(self, module, **options): """Registers a module with this application. The keyword argument @@ -1184,7 +1190,7 @@ class Flask(_PackageBoundObject): """ ctx = _request_ctx_stack.top bp = ctx.request.blueprint - if not isinstance(ctx.session, _NullSession): + if not self.session_interface.is_null_session(ctx.session): self.save_session(ctx.session, response) funcs = () if bp is not None and bp in self.after_request_funcs: diff --git a/flask/ctx.py b/flask/ctx.py index 3ea43b97..a189b28f 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -122,7 +122,7 @@ class RequestContext(object): # request context (e.g. flask-sqlalchemy). self.session = self.app.open_session(self.request) if self.session is None: - self.session = _NullSession() + self.session = self.app.make_null_session() def pop(self): """Pops the request context and unbinds it by doing that. This will diff --git a/flask/session.py b/flask/session.py index df2d8773..bfe196b0 100644 --- a/flask/session.py +++ b/flask/session.py @@ -3,41 +3,17 @@ flask.session ~~~~~~~~~~~~~ - Implements cookie based sessions based on Werkzeug's secure cookie - system. + This module used to flask with the session global so we moved it + over to flask.sessions :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from werkzeug.contrib.securecookie import SecureCookie +from warnings import warn +warn(DeprecationWarning('please use flask.sessions instead')) +from .sessions import * -class Session(SecureCookie): - """Expands the session with support for switching between permanent - and non-permanent sessions. - """ - - def _get_permanent(self): - return self.get('_permanent', False) - - def _set_permanent(self, value): - self['_permanent'] = bool(value) - - permanent = property(_get_permanent, _set_permanent) - del _get_permanent, _set_permanent - - -class _NullSession(Session): - """Class used to generate nicer error messages if sessions are not - available. Will still allow read-only access to the empty session - but fail on setting. - """ - - def _fail(self, *args, **kwargs): - raise RuntimeError('the session is unavailable because no secret ' - 'key was set. Set the secret_key on the ' - 'application to something unique and secret.') - __setitem__ = __delitem__ = clear = pop = popitem = \ - update = setdefault = _fail - del _fail +Session = SecureCookieSession +_NullSession = NullSession From 10c99e95e98844ba191e4767550b0dc19fa6d34c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 12:25:25 +0200 Subject: [PATCH 0697/3747] Added missing sessions module --- flask/sessions.py | 168 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 flask/sessions.py diff --git a/flask/sessions.py b/flask/sessions.py new file mode 100644 index 00000000..c082b237 --- /dev/null +++ b/flask/sessions.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +""" + flask.sessions + ~~~~~~~~~~~~~~ + + Implements cookie based sessions based on Werkzeug's secure cookie + system. + + :copyright: (c) 2010 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from datetime import datetime +from werkzeug.contrib.securecookie import SecureCookie + + +class SessionMixin(object): + """Expands a basic dictionary with an accessors that are expected + by Flask extensions and users for the session. + """ + + def _get_permanent(self): + return self.get('_permanent', False) + + def _set_permanent(self, value): + self['_permanent'] = bool(value) + + permanent = property(_get_permanent, _set_permanent) + del _get_permanent, _set_permanent + + new = False + modified = True + + +class SecureCookieSession(SecureCookie, SessionMixin): + """Expands the session with support for switching between permanent + and non-permanent sessions. + """ + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args, **kwargs): + raise RuntimeError('the session is unavailable because no secret ' + 'key was set. Set the secret_key on the ' + 'application to something unique and secret.') + __setitem__ = __delitem__ = clear = pop = popitem = \ + update = setdefault = _fail + del _fail + + +class SessionInterface(object): + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns `None` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + .. versionadded:: 0.7 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + def make_null_session(self, app): + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj): + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_domain(self, app): + """Helpful helper method that returns the cookie domain that should + be used for the session cookie if session cookies are used. + """ + if app.config['SERVER_NAME'] is not None: + # chop of the port which is usually not supported by browsers + return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + + def get_expiration_time(self, app, session): + """A helper method that returns an expiration date for the session + or `None` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.utcnow() + app.permanent_session_lifetime + + def open_session(self, app, request): + """This method has to be implemented and must either return `None` + in case the loading failed because of a configuration error or an + instance of a session object which implements a dictionary like + interface + the methods and attributes on :class:`SessionMixin`. + """ + raise NotImplementedError() + + def save_session(self, app, session, response): + """This is called for actual sessions returned by :meth:`open_session` + at the end of the request. This is still called during a request + context so if you absolutely need access to the request you can do + that. + """ + raise NotImplementedError() + + +class SecureCookieSessionInterface(SessionInterface): + """The cookie session interface that uses the Werkzeug securecookie + as client side session backend. + """ + session_class = SecureCookieSession + + def open_session(self, app, request): + key = app.secret_key + if key is not None: + return self.session_class.load_cookie(request, + app.session_cookie_name, + secret_key=key) + + def save_session(self, app, session, response): + expires = self.get_expiration_time(app, session) + domain = self.get_cookie_domain(app) + if session.modified and not session: + response.delete_cookie(app.session_cookie_name, + domain=domain) + else: + session.save_cookie(response, app.session_cookie_name, + expires=expires, httponly=True, domain=domain) From 5cbfbd229d4cc524e33b1e922b5297264227ed74 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 12:29:58 +0200 Subject: [PATCH 0698/3747] Documented a change in the session system --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 0db9003e..c22a6626 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Relase date to be decided, codename to be chosen. - Refactored session support into a session interface so that the implementation of the sessions can be changed without having to override the Flask class. +- Empty session cookies are now deleted properly automatically. Version 0.7.2 ------------- From 7290981cef49058b9c53f8a4465cf85537c0d22d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 12:31:57 +0200 Subject: [PATCH 0699/3747] More docstrings for the session mixin --- flask/sessions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask/sessions.py b/flask/sessions.py index c082b237..abad920a 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -25,10 +25,17 @@ class SessionMixin(object): def _set_permanent(self, value): self['_permanent'] = bool(value) + #: this reflects the ``'_permanent'`` key in the dict. permanent = property(_get_permanent, _set_permanent) del _get_permanent, _set_permanent + #: some session backends can tell you if a session is new, but that is + #: not necessarily guaranteed. Use with caution. new = False + + #: for some backends this will always be `True`, but some backends will + #: default this to false and detect changes in the dictionary for as + #: long as changes do not happen on mutable structures in the session. modified = True From 10f8dc18b8d43c2776c9381f681263778674bf0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 13:11:32 +0200 Subject: [PATCH 0700/3747] More doc updates --- flask/sessions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/sessions.py b/flask/sessions.py index abad920a..8f59a4f7 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -30,12 +30,14 @@ class SessionMixin(object): del _get_permanent, _set_permanent #: some session backends can tell you if a session is new, but that is - #: not necessarily guaranteed. Use with caution. + #: not necessarily guaranteed. Use with caution. The default mixin + #: implementation just hardcodes `False` in. new = False #: for some backends this will always be `True`, but some backends will #: default this to false and detect changes in the dictionary for as #: long as changes do not happen on mutable structures in the session. + #: The default mixin implementation just hardcodes `True` in. modified = True From f29ec355e99e052f98df46875aeb8e319b8e1beb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jul 2011 13:14:15 +0200 Subject: [PATCH 0701/3747] Backwards compatibility import --- docs/upgrading.rst | 10 ++++++++++ flask/__init__.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index df084e51..154b51a7 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,16 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.8 +----------- + +Flask introduced a new session interface system. We also noticed that +there was a naming collision between `flask.session` the module that +implements sessions and :data:`flask.session` which is the global session +object. With that introduction we moved the implementation details for +the session system into a new module called :mod:`flask.sessions`. If you +used the previously undocumented session support we urge you to upgrade. + Version 0.7 ----------- diff --git a/flask/__init__.py b/flask/__init__.py index 930859da..88bf40e8 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -35,3 +35,6 @@ from .signals import signals_available, template_rendered, request_started, \ # only import json if it's available if json_available: from .helpers import json + +# backwards compat, goes away in 1.0 +from .sessions import SecureCookieSession as Session From deb513c7fe5a285e2bdeff99424e243d7b4f7829 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 8 Jul 2011 14:38:49 +0200 Subject: [PATCH 0702/3747] The session interface is new in 0.8, not 0.7 --- docs/api.rst | 2 +- flask/sessions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 99071f87..b5254996 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -198,7 +198,7 @@ To access the current session you can use the :class:`session` object: Session Interface ----------------- -.. versionadded:: 0.7 +.. versionadded:: 0.8 The session interface provides a simple way to replace the session implementation that Flask is using. diff --git a/flask/sessions.py b/flask/sessions.py index 8f59a4f7..ee006cda 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -89,7 +89,7 @@ class SessionInterface(object): app = Flask(__name__) app.session_interface = MySessionInterface() - .. versionadded:: 0.7 + .. versionadded:: 0.8 """ #: :meth:`make_null_session` will look here for the class that should From 585bf02ee0d23d020f8a24db645c308d912b62e0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 8 Jul 2011 19:20:40 +0200 Subject: [PATCH 0703/3747] Better session logic. --- flask/sessions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index ee006cda..e3fd7fb9 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -169,9 +169,10 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) - if session.modified and not session: - response.delete_cookie(app.session_cookie_name, - domain=domain) - else: - session.save_cookie(response, app.session_cookie_name, - expires=expires, httponly=True, domain=domain) + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain) + return + session.save_cookie(response, app.session_cookie_name, + expires=expires, httponly=True, domain=domain) From d90b1f1705b7bda17743406b4eac5511bdbb702a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 8 Jul 2011 19:21:50 +0200 Subject: [PATCH 0704/3747] Backout last change, save_cookie by itself does that --- flask/sessions.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index e3fd7fb9..ee006cda 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -169,10 +169,9 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) - if not session: - if session.modified: - response.delete_cookie(app.session_cookie_name, - domain=domain) - return - session.save_cookie(response, app.session_cookie_name, - expires=expires, httponly=True, domain=domain) + if session.modified and not session: + response.delete_cookie(app.session_cookie_name, + domain=domain) + else: + session.save_cookie(response, app.session_cookie_name, + expires=expires, httponly=True, domain=domain) From ebb65b81e1279060bd75d211e5e32aecf0fa3932 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 10 Jul 2011 13:33:52 +0200 Subject: [PATCH 0705/3747] Removed an unnecessary import. --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 162ce4f2..48369ee4 100644 --- a/flask/app.py +++ b/flask/app.py @@ -13,7 +13,7 @@ from __future__ import with_statement import sys from threading import Lock -from datetime import timedelta, datetime +from datetime import timedelta from itertools import chain from werkzeug import ImmutableDict From 99c2defb431642f36d6869ff84922dced06c158a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 10 Jul 2011 13:34:21 +0200 Subject: [PATCH 0706/3747] Added an example of how to postprocess requests in the testing docs. --- docs/testing.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index a196e2b7..098e81be 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -236,6 +236,20 @@ need to call :meth:`~flask.Flask.preprocess_request` yourself:: This can be necessary to open database connections or something similar depending on how your application was designed. +If you want to call the :meth:`~flask.Flask.after_request` functions you +need to call into :meth:`~flask.Flask.process_response` which however +requires that you pass it a response object:: + + app = flask.Flask(__name__) + + with app.test_request_context('/?name=Peter'): + resp = Response('...') + resp = app.process_response(resp) + ... + +This in general is less useful because at that point you can directly +start using the test client. + Keeping the Context Around -------------------------- From c8663e8dab230325037f8988880945340c331486 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 10 Jul 2011 13:35:26 +0200 Subject: [PATCH 0707/3747] Fixed a typo in the docs. --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index 098e81be..ed5765ea 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -221,7 +221,7 @@ application factories (see :ref:`app-factories`). Note however that if you are using a test request context, the :meth:`~flask.Flask.before_request` functions are not automatically called -same fore :meth:`~flask.Flask.after_request` functions. However +same for :meth:`~flask.Flask.after_request` functions. However :meth:`~flask.Flask.teardown_request` functions are indeed executed when the test request context leaves the `with` block. If you do want the :meth:`~flask.Flask.before_request` functions to be called as well, you From 441c8a5b93aca67f61d584950f351e144f48b10d Mon Sep 17 00:00:00 2001 From: ThomasWaldmann Date: Sun, 10 Jul 2011 09:25:08 -0700 Subject: [PATCH 0708/3747] fixed typo --- docs/patterns/urlprocessors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst index f00211a6..778a5a6b 100644 --- a/docs/patterns/urlprocessors.rst +++ b/docs/patterns/urlprocessors.rst @@ -32,7 +32,7 @@ Consider an application like this:: g.lang_code = lang_code ... -This is an awful lot of reptition as you have to handle the language code +This is an awful lot of repetition as you have to handle the language code setting on the :data:`~flask.g` object yourself in every single function. Sure, a decorator could be used to simplify this, but if you want to generate URLs from one function to another you would have to still provide From e140bad3f8cceb464d190c0abb4e02895f5c5f3d Mon Sep 17 00:00:00 2001 From: ThomasWaldmann Date: Sun, 10 Jul 2011 09:28:26 -0700 Subject: [PATCH 0709/3747] fixed typo --- docs/reqcontext.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 832f0e66..3b49e1d5 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -157,7 +157,7 @@ request handling as they are bound to the lifecycle of the popped, the :meth:`~flask.Flask.teardown_request` functions are called. This is important to know if the life of the request context is prolonged -by using the test client in a with statement of when using the request +by using the test client in a with statement or when using the request context from the command line:: with app.test_client() as client: From fd679638ea991fbdd5d7318bc9451f0bd5532224 Mon Sep 17 00:00:00 2001 From: Jesse Dubay Date: Mon, 11 Jul 2011 15:25:37 -0700 Subject: [PATCH 0710/3747] typo fix: flask -> flag --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 48369ee4..47254bed 100644 --- a/flask/app.py +++ b/flask/app.py @@ -112,7 +112,7 @@ class Flask(_PackageBoundObject): #: configuration key. Defaults to `False`. debug = ConfigAttribute('DEBUG') - #: The testing flask. Set this to `True` to enable the test mode of + #: The testing flag. Set this to `True` to enable the test mode of #: Flask extensions (and in the future probably also Flask itself). #: For example this might activate unittest helpers that have an #: additional runtime cost which should not be enabled by default. From 1a249e338927efe58228bc70bfa3cf754526ff8a Mon Sep 17 00:00:00 2001 From: consigliere Date: Mon, 11 Jul 2011 15:28:11 -0700 Subject: [PATCH 0711/3747] Corrected decorators to remove duplicate teardown_request() and add after_request() --- docs/tutorial/dbcon.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 8f9e4595..b19cb14c 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -9,7 +9,7 @@ connection in all our functions so it makes sense to initialize them before each request and shut them down afterwards. Flask allows us to do that with the :meth:`~flask.Flask.before_request`, -:meth:`~flask.Flask.teardown_request` and :meth:`~flask.Flask.teardown_request` +:meth:`~flask.Flask.after_request` and :meth:`~flask.Flask.teardown_request` decorators:: @app.before_request From 7dcf6cbc3141d100e229b4a781f67ab1286af055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bartoszkiewicz?= Date: Tue, 12 Jul 2011 21:51:53 +0200 Subject: [PATCH 0712/3747] Don't use deprecated flask.session in flask.ctx. --- flask/ctx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index a189b28f..0943d10a 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -12,7 +12,6 @@ from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack -from .session import _NullSession from .module import blueprint_is_module From 9d899cd63e539b92262bbc6f101e7b5408bb31fc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 13 Jul 2011 14:04:23 +0200 Subject: [PATCH 0713/3747] Fixed a typo. This closes #278 --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 48369ee4..a48e8f63 100644 --- a/flask/app.py +++ b/flask/app.py @@ -82,7 +82,7 @@ class Flask(_PackageBoundObject): extension will look for the code in your application that triggered an SQL query in debug mode. If the import name is not properly set up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplicaiton.app` and not + pick up SQL queries in `yourapplication.app` and not `yourapplication.views.frontend`) .. versionadded:: 0.5 From 3b31df81aeccb889810b4b49ce6e608e67ebeeea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 14 Jul 2011 14:18:42 +0200 Subject: [PATCH 0714/3747] View functions can opt out of the default OPTIONS implementation --- CHANGES | 2 ++ flask/app.py | 17 +++++++++++++---- tests/flask_tests.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index c22a6626..f9f3c82d 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,8 @@ Relase date to be decided, codename to be chosen. the implementation of the sessions can be changed without having to override the Flask class. - Empty session cookies are now deleted properly automatically. +- View functions can now opt out of getting the automatic + OPTIONS implementation. Version 0.7.2 ------------- diff --git a/flask/app.py b/flask/app.py index 67e8298a..db9d2af0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -703,15 +703,24 @@ class Flask(_PackageBoundObject): endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint methods = options.pop('methods', None) + # if the methods are not given and the view_func object knows its # methods we can use that instead. If neither exists, we go with # a tuple of only `GET` as default. if methods is None: methods = getattr(view_func, 'methods', None) or ('GET',) - provide_automatic_options = False - if 'OPTIONS' not in methods: - methods = tuple(methods) + ('OPTIONS',) - provide_automatic_options = True + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + provide_automatic_options = getattr(view_func, + 'provide_automatic_options', None) + + if provide_automatic_options is None: + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + else: + provide_automatic_options = False # due to a werkzeug bug we need to make sure that the defaults are # None if they are an empty dictionary. This should not be necessary diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e13dfd36..095f4bad 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -194,6 +194,23 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = app.test_client().open('/', method='OPTIONS') assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + def test_options_handling_disabled(self): + app = flask.Flask(__name__) + def index(): + return 'Hello World!' + index.provide_automatic_options = False + app.route('/')(index) + rv = app.test_client().open('/', method='OPTIONS') + assert rv.status_code == 405 + + app = flask.Flask(__name__) + def index2(): + return 'Hello World!' + index2.provide_automatic_options = True + app.route('/', methods=['OPTIONS'])(index2) + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['OPTIONS'] + def test_request_dispatching(self): app = flask.Flask(__name__) @app.route('/') From 76796c326d9abc243034847c41aa61f80ba3d653 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 14 Jul 2011 14:25:14 +0200 Subject: [PATCH 0715/3747] Documented view function options --- docs/api.rst | 38 ++++++++++++++++++++++++++++++++++++++ flask/app.py | 4 ++++ 2 files changed, 42 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index b5254996..94a38ad5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -451,3 +451,41 @@ Class Based Views .. autoclass:: flask.views.MethodView :members: + +.. _view-func-options: + +View Function Options +--------------------- + +For internal usage the view functions can have some attributes attached to +customize behavior the view function would normally not have control over. +The following attributes can be provided optionally to either override +some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: + +- `__name__`: The name of a function is by default used as endpoint. If + endpoint is provided explicitly this value is used. Additionally this + will be prefixed with the name of the blueprint by default which + cannot be customized from the function itself. + +- `methods`: If methods are not provided when the URL rule is added, + Flask will look on the view function object itself is an `methods` + attribute exists. If it does, it will pull the information for the + methods from there. + +- `provide_automatic_options`: if this attribute is set Flask will + either force enable or disable the automatic implementation of the + HTTP `OPTIONS` response. This can be useful when working with + decorators that want to customize the `OPTIONS` response on a per-view + basis. + +Full example:: + + def index(): + if request.method == 'OPTIONS': + # custom options handling here + ... + return 'Hello World!' + index.provide_automatic_options = False + index.methods = ['GET', 'OPTIONS'] + + app.add_url_rule('/', index) diff --git a/flask/app.py b/flask/app.py index db9d2af0..8ca74e30 100644 --- a/flask/app.py +++ b/flask/app.py @@ -678,6 +678,10 @@ class Flask(_PackageBoundObject): app.view_functions['index'] = index + If a view function is provided some defaults can be specified directly + on the view function. For more information refer to + :ref:`view-func-options`. + .. versionchanged:: 0.2 `view_func` parameter added. From 6ca321c97989705b354ecb4fa06aed3ca4360542 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 14 Jul 2011 14:25:50 +0200 Subject: [PATCH 0716/3747] Added a versionadded --- docs/api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 94a38ad5..ddeed4a6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -489,3 +489,6 @@ Full example:: index.methods = ['GET', 'OPTIONS'] app.add_url_rule('/', index) + +.. versionadded:: 0.8 + The `provide_automatic_options` functionality was added. From 2866ccda1fe19a42adc735d75abaf304f8aed27b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 15 Jul 2011 18:03:48 +0200 Subject: [PATCH 0717/3747] Switch to explicit Werkzeug imports --- flask/__init__.py | 3 ++- flask/app.py | 6 +++--- flask/config.py | 2 +- flask/globals.py | 2 +- flask/helpers.py | 8 +++++++- flask/testing.py | 2 +- flask/wrappers.py | 4 ++-- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 88bf40e8..47bf3cab 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -14,7 +14,8 @@ __version__ = '0.8-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. -from werkzeug import abort, redirect +from werkzeug.exceptions import abort +from werkzeug.utils import redirect from jinja2 import Markup, escape from .app import Flask, Request, Response diff --git a/flask/app.py b/flask/app.py index 8ca74e30..b4e3d647 100644 --- a/flask/app.py +++ b/flask/app.py @@ -16,7 +16,7 @@ from threading import Lock from datetime import timedelta from itertools import chain -from werkzeug import ImmutableDict +from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed @@ -551,7 +551,7 @@ class Flask(_PackageBoundObject): Werkzeug server. See :func:`werkzeug.run_simple` for more information. """ - from werkzeug import run_simple + from werkzeug.serving import run_simple if 'debug' in options: self.debug = options.pop('debug') options.setdefault('use_reloader', self.debug) @@ -1267,7 +1267,7 @@ class Flask(_PackageBoundObject): :func:`werkzeug.create_environ` for more information, this function accepts the same arguments). """ - from werkzeug import create_environ + from werkzeug.test import create_environ environ_overrides = kwargs.setdefault('environ_overrides', {}) if self.config.get('SERVER_NAME'): server_name = self.config.get('SERVER_NAME') diff --git a/flask/config.py b/flask/config.py index bb2d6e9e..06dd02e2 100644 --- a/flask/config.py +++ b/flask/config.py @@ -15,7 +15,7 @@ import imp import os import errno -from werkzeug import import_string +from werkzeug.utils import import_string class ConfigAttribute(object): diff --git a/flask/globals.py b/flask/globals.py index 84714105..34099263 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -11,7 +11,7 @@ """ from functools import partial -from werkzeug import LocalStack, LocalProxy +from werkzeug.local import LocalStack, LocalProxy def _lookup_object(name): top = _request_ctx_stack.top diff --git a/flask/helpers.py b/flask/helpers.py index 2a841236..f44a7f64 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -36,9 +36,15 @@ except ImportError: json_available = False -from werkzeug import Headers, wrap_file +from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound +# this was moved in 0.7 +try: + from werkzeug.wsgi import wrap_file +except ImportError: + from werkzeug.utils import wrap_file + from jinja2 import FileSystemLoader from .globals import session, _request_ctx_stack, current_app, request diff --git a/flask/testing.py b/flask/testing.py index 84237336..06a2c016 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -from werkzeug import Client, EnvironBuilder +from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack diff --git a/flask/wrappers.py b/flask/wrappers.py index e592cc28..d5f6ed7d 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -9,8 +9,8 @@ :license: BSD, see LICENSE for more details. """ -from werkzeug import Request as RequestBase, Response as ResponseBase, \ - cached_property +from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.utils import cached_property from .helpers import json, _assert_have_json from .globals import _request_ctx_stack From 8d2daea327c2d0654004c2be9cb2eae074bbb80a Mon Sep 17 00:00:00 2001 From: Felix Hummel Date: Thu, 14 Jul 2011 21:18:53 +0200 Subject: [PATCH 0718/3747] be consistent with app.config['UPLOAD_FOLDER'] Signed-off-by: Armin Ronacher --- docs/patterns/fileuploads.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index ab49cedb..d237b107 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -28,6 +28,7 @@ bootstrapping code for our application:: ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) app = Flask(__name__) + app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER So first we need a couple of imports. Most should be straightforward, the :func:`werkzeug.secure_filename` is explained a little bit later. The @@ -58,7 +59,7 @@ the file and redirects the user to the URL for the uploaded file:: file = request.files['file'] if file and allowed_file(file.filename): filename = secure_filename(file.filename) - file.save(os.path.join(UPLOAD_FOLDER, filename)) + file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return redirect(url_for('uploaded_file', filename=filename)) return ''' @@ -116,7 +117,7 @@ older versions of Flask:: app.add_url_rule('/uploads/', 'uploaded_file', build_only=True) app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { - '/uploads': UPLOAD_FOLDER + '/uploads': app.config['UPLOAD_FOLDER'] }) If you now run the application everything should work as expected. From c9a2ad2b8d079c0ecca701f4c0b15390437c4843 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 16 Jul 2011 01:16:03 +0200 Subject: [PATCH 0719/3747] Fixed a bug in list_templates --- CHANGES | 8 ++++++++ flask/templating.py | 4 +++- tests/blueprintapp/__init__.py | 4 ++-- tests/blueprintapp/apps/admin/__init__.py | 4 +++- tests/blueprintapp/apps/frontend/__init__.py | 2 +- tests/flask_tests.py | 6 ++++++ 6 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index f9f3c82d..4f400d8e 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,14 @@ Relase date to be decided, codename to be chosen. - View functions can now opt out of getting the automatic OPTIONS implementation. +Version 0.7.3 +------------- + +Bugfix release, release date to be decided + +- Fixed the Jinja2 environment's list_templates method not returning the + correct names when blueprints or modules were involved. + Version 0.7.2 ------------- diff --git a/flask/templating.py b/flask/templating.py index 147f49d0..d38d3824 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -78,6 +78,8 @@ class DispatchingJinjaLoader(BaseLoader): pass for blueprint in self.app.blueprints.itervalues(): + if blueprint_is_module(blueprint): + continue loader = blueprint.jinja_loader if loader is not None: yield loader, template @@ -93,7 +95,7 @@ class DispatchingJinjaLoader(BaseLoader): if loader is not None: for template in loader.list_templates(): prefix = '' - if not blueprint_is_module(blueprint): + if blueprint_is_module(blueprint): prefix = name + '/' result.add(prefix + template) diff --git a/tests/blueprintapp/__init__.py b/tests/blueprintapp/__init__.py index fa76807c..2b8ef75d 100644 --- a/tests/blueprintapp/__init__.py +++ b/tests/blueprintapp/__init__.py @@ -1,7 +1,7 @@ from flask import Flask app = Flask(__name__) -from moduleapp.apps.admin import admin -from moduleapp.apps.frontend import frontend +from blueprintapp.apps.admin import admin +from blueprintapp.apps.frontend import frontend app.register_blueprint(admin) app.register_blueprint(frontend) diff --git a/tests/blueprintapp/apps/admin/__init__.py b/tests/blueprintapp/apps/admin/__init__.py index fe33e3e9..3f714d95 100644 --- a/tests/blueprintapp/apps/admin/__init__.py +++ b/tests/blueprintapp/apps/admin/__init__.py @@ -1,6 +1,8 @@ from flask import Blueprint, render_template -admin = Blueprint(__name__, url_prefix='/admin') +admin = Blueprint('admin', __name__, url_prefix='/admin', + template_folder='templates', + static_folder='static') @admin.route('/') diff --git a/tests/blueprintapp/apps/frontend/__init__.py b/tests/blueprintapp/apps/frontend/__init__.py index e98ff280..69c8666a 100644 --- a/tests/blueprintapp/apps/frontend/__init__.py +++ b/tests/blueprintapp/apps/frontend/__init__.py @@ -1,6 +1,6 @@ from flask import Blueprint, render_template -frontend = Blueprint(__name__) +frontend = Blueprint('frontend', __name__, template_folder='templates') @frontend.route('/') diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 095f4bad..7e00d73e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1414,6 +1414,12 @@ class BlueprintTestCase(unittest.TestCase): with flask.Flask(__name__).test_request_context(): assert flask.render_template('nested/nested.txt') == 'I\'m nested' + def test_templates_list(self): + from blueprintapp import app + templates = sorted(app.jinja_env.list_templates()) + self.assertEqual(templates, ['admin/index.html', + 'frontend/index.html']) + def test_dotted_names(self): frontend = flask.Blueprint('myapp.frontend', __name__) backend = flask.Blueprint('myapp.backend', __name__) From 13cddba84f90d28899d1f0f7c2c9bcc901cee64c Mon Sep 17 00:00:00 2001 From: Ustun Ozgur Date: Mon, 18 Jul 2011 06:49:53 -0700 Subject: [PATCH 0720/3747] Typo: developped -> developed --- README | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README b/README index 56d57f88..5c0eb4f1 100644 --- a/README +++ b/README @@ -1,4 +1,3 @@ - // Flask // web development, one drop at a time @@ -8,7 +7,7 @@ Flask is a microframework for Python based on Werkzeug and Jinja2. It's intended for small scale applications - and was developped with best intentions in mind. + and was developed with best intentions in mind. ~ Is it ready? From 343e678900a9970857f37b53b5d2f069327cd828 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 Jul 2011 23:06:15 +0200 Subject: [PATCH 0721/3747] Fixed a typo in a docstring --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index b4e3d647..6a5a417b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -856,7 +856,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 One can now additionally also register custom exception types that do not necessarily have to be a subclass of the - :class:~`werkzeug.exceptions.HTTPException` class. + :class:`~werkzeug.exceptions.HTTPException` class. :param code: the code as integer for the handler """ From 290925e13348385cd3508d9ae52ea328ee9990d9 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Tue, 19 Jul 2011 16:42:11 -0500 Subject: [PATCH 0722/3747] Minor documentation typo / grammar fixes --- docs/patterns/templateinheritance.rst | 2 +- flask/helpers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/templateinheritance.rst b/docs/patterns/templateinheritance.rst index 8a1a306d..70015ecc 100644 --- a/docs/patterns/templateinheritance.rst +++ b/docs/patterns/templateinheritance.rst @@ -38,7 +38,7 @@ document that you might use for a simple two-column page. It's the job of In this example, the ``{% block %}`` tags define four blocks that child templates -can fill in. All the `block` tag does is to tell the template engine that a +can fill in. All the `block` tag does is tell the template engine that a child template may override those portions of the template. Child Template diff --git a/flask/helpers.py b/flask/helpers.py index f44a7f64..dff79f68 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -421,7 +421,7 @@ def safe_join(directory, filename): :param directory: the base directory. :param filename: the untrusted filename relative to that directory. - :raises: :class:`~werkzeug.exceptions.NotFound` if the retsulting path + :raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path would fall out of `directory`. """ filename = posixpath.normpath(filename) From d5cfcfeba7313d80d7ac29adb8c54960cb594079 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sat, 23 Jul 2011 11:13:30 -0700 Subject: [PATCH 0723/3747] Intersphinx link fixes. --- flask/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index b4e3d647..d6d95350 100644 --- a/flask/app.py +++ b/flask/app.py @@ -548,8 +548,9 @@ class Flask(_PackageBoundObject): to have the server available externally as well. :param port: the port of the webserver :param options: the options to be forwarded to the underlying - Werkzeug server. See :func:`werkzeug.run_simple` - for more information. + Werkzeug server. See + :func:`werkzeug.serving.run_simple` for more + information. """ from werkzeug.serving import run_simple if 'debug' in options: @@ -1264,7 +1265,7 @@ class Flask(_PackageBoundObject): def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see - :func:`werkzeug.create_environ` for more information, this + :func:`werkzeug.test.EnvironBuilder` for more information, this function accepts the same arguments). """ from werkzeug.test import create_environ From c240a1f78101c57591bd23f26a367d214d533eac Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 26 Jul 2011 16:32:36 -0400 Subject: [PATCH 0724/3747] Add code organization help to tutorial doc. As suggested by burhan on #pocoo today. --- docs/tutorial/dbcon.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index b19cb14c..99391a27 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -39,3 +39,19 @@ environments. That special :data:`~flask.g` object does some magic behind the scenes to ensure it does the right thing. Continue to :ref:`tutorial-views`. + +.. hint:: Where do I put this code? + + If you've been following along in this tutorial, you might be wondering + where to put the code from this step and the next. A logical place is to + group these module-level functions together, and put your new + ``before_request`` and ``teardown_request`` functions below your existing + ``init_db`` function (following the tutorial line-by-line). + + If you need a moment to find your bearings, take a look at how the `example + source`_ is organized. In Flask, you can put all of your application code + into a single Python module. You don't have to, and if your app :ref:`grows + larger `, it's a good idea not to. + +.. _example source: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ From 4d2c8181b4283e3e4d322739f49137e5553f8315 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 27 Jul 2011 22:34:18 +0200 Subject: [PATCH 0725/3747] Updated docs for streaming --- docs/patterns/index.rst | 1 + docs/patterns/streaming.rst | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 docs/patterns/streaming.rst diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 4233808b..3a1e409c 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -35,3 +35,4 @@ Snippet Archives `_. lazyloading mongokit favicon + streaming diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst new file mode 100644 index 00000000..dbc3e921 --- /dev/null +++ b/docs/patterns/streaming.rst @@ -0,0 +1,70 @@ +Streaming Contents +================== + +Sometimes you want to send an enormous amount of data to the client, much +more than you want to keep in memory. When you are generating the data on +the fly though, how do you send that back to the client without the +roundtrip to the filesystem? + +The answer is by using generators and direct responses. + +Basic Usage +----------- + +This is a basic view function that generates a lot of CSV data on the fly. +The trick is to have an inner function that uses a generator to generate +data and to then invoke that function and pass it to a response object +that has the ``direct_passthrough`` flag set. This flag is used to inform +the system that data is generated on the fly and should be passed through +without buffering: + +.. sourcecode:: python + + from flask import Response + + @app.route('/large.csv') + def generate_large_csv(): + def generate(): + for row in iter_all_rows(): + yield ','.join(row) + '\n' + return Response(generate(), direct_passthrough=True, + mimetype='text/csv') + +Each ``yield`` expression is directly sent to the browser. Now though +that some WSGI middlewares might break streaming, so be careful there in +debug environments with profilers and other things you might have enabled. + +Streaming from Templates +------------------------ + +The Jinja2 template engine also supports rendering templates piece by +piece. This functionality is not directly exposed by Flask because it is +quite uncommon, but you can easily do it yourself: + +.. sourcecode:: python + + from flask import Response + + def stream_template(template_name, **context): + app.update_template_context(context) + t = app.jinja_env.get_template(template_name) + rv = t.stream(context) + rv.enable_buffering(5) + return rv + + @app.route('/my-large-page.html') + def render_large_template(): + rows = iter_all_rows() + return Response(stream_template('the_template.html', rows=rows), + direct_passthrough=True) + +The trick here is to get the template object from the Jinja2 environment +on the application and to call :meth:`~jinja2.Template.stream` instead of +:meth:`~jinja2.Template.render` which returns a stream object instead of a +string. Since we're bypassing the Flask template render functions and +using the template object itself we have to make sure to update the render +context ourselves by calling :meth:`~flask.Flask.update_template_context`. +The template is then evaluated as the stream is iterated over. Since each +time you do a yield the server will flush the content to the client you +might want to buffer up a few items in the template which you can do with +``rv.enable_buffering(size)``. ``5`` is a sane default. From 029eebe2f57ed460c3e72d41422f3ffd01f0d4c1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 27 Jul 2011 22:34:56 +0200 Subject: [PATCH 0726/3747] No need for the directive here. --- docs/patterns/streaming.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index dbc3e921..1998799f 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -16,9 +16,7 @@ The trick is to have an inner function that uses a generator to generate data and to then invoke that function and pass it to a response object that has the ``direct_passthrough`` flag set. This flag is used to inform the system that data is generated on the fly and should be passed through -without buffering: - -.. sourcecode:: python +without buffering:: from flask import Response @@ -39,9 +37,7 @@ Streaming from Templates The Jinja2 template engine also supports rendering templates piece by piece. This functionality is not directly exposed by Flask because it is -quite uncommon, but you can easily do it yourself: - -.. sourcecode:: python +quite uncommon, but you can easily do it yourself:: from flask import Response From a7b85c9f15cccc0f4bd9560ecf7c11af8d3b7b53 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Fri, 29 Jul 2011 09:54:50 -0300 Subject: [PATCH 0727/3747] Consistent quotes --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 5eaaf9c5..baca5050 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -18,7 +18,7 @@ A minimal Flask application looks something like this:: @app.route('/') def hello_world(): - return "Hello World!" + return 'Hello World!' if __name__ == '__main__': app.run() From 153d727d1f8396b5486dccb48c033e109cba93d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 30 Jul 2011 02:07:31 +0200 Subject: [PATCH 0728/3747] Fixed the docs for the request object. They were incomplete --- docs/api.rst | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ddeed4a6..ff7e597a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -29,20 +29,7 @@ Incoming Request Data --------------------- .. autoclass:: Request - -.. class:: request - - To access incoming request data, you can use the global `request` - object. Flask parses incoming request data for you and gives you - access to it through that global object. Internally Flask makes - sure that you always get the correct data for the active thread if you - are in a multithreaded environment. - - This is a proxy. See :ref:`notes-on-proxies` for more information. - - The request object is an instance of a :class:`~werkzeug.wrappers.Request` - subclass and provides all of the attributes Werkzeug defines. This - just shows a quick overview of the most important ones. + :members: .. attribute:: form @@ -134,6 +121,21 @@ Incoming Request Data the incoming data was `application/json`. This requires Python 2.6 or an installed version of simplejson. +.. class:: request + + To access incoming request data, you can use the global `request` + object. Flask parses incoming request data for you and gives you + access to it through that global object. Internally Flask makes + sure that you always get the correct data for the active thread if you + are in a multithreaded environment. + + This is a proxy. See :ref:`notes-on-proxies` for more information. + + The request object is an instance of a :class:`~werkzeug.wrappers.Request` + subclass and provides all of the attributes Werkzeug defines. This + just shows a quick overview of the most important ones. + + Response Objects ---------------- From eba2f70c6b95d9d61b5854feea64f0ba1fb620d2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 30 Jul 2011 02:11:30 +0200 Subject: [PATCH 0729/3747] Clarified a sentence that no longer made sense with the new docs on request objects. --- flask/wrappers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flask/wrappers.py b/flask/wrappers.py index d5f6ed7d..37d972d1 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -23,6 +23,10 @@ class Request(RequestBase): It is what ends up as :class:`~flask.request`. If you want to replace the request object used you can subclass this and set :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. """ #: the internal URL rule that matched the request. This can be From c726e5fee0ce7ec3a0a0aab00cbd18b26c31be9d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 30 Jul 2011 11:02:36 -0400 Subject: [PATCH 0730/3747] Fix lighttpd config, from giskard on #pocoo. --- docs/deploying/fastcgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 0b5d887c..6dace1a8 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -62,7 +62,7 @@ A basic FastCGI configuration for lighttpd looks like that:: "socket" => "/tmp/yourapplication-fcgi.sock", "bin-path" => "/var/www/yourapplication/yourapplication.fcgi", "check-local" => "disable", - "max-procs" -> 1 + "max-procs" => 1 )) ) From fafcc02f261e90a006efd5cc4dfa876531af9594 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 4 Aug 2011 16:44:42 +0200 Subject: [PATCH 0731/3747] Added a testcase for 404 errors caused by the routing system --- tests/flask_tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7e00d73e..fb3e0e52 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -564,6 +564,18 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert rv.status_code == 500 assert 'internal server error' == rv.data + def test_before_request_and_routing_errors(self): + app = flask.Flask(__name__) + @app.before_request + def attach_something(): + flask.g.something = 'value' + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + rv = app.test_client().get('/') + assert rv.status_code == 404 + assert rv.data == 'value' + def test_user_error_handling(self): class MyException(Exception): pass From 5304a15adc2ff1f19b889a40d039008404b25a61 Mon Sep 17 00:00:00 2001 From: Brandon Stafford Date: Thu, 4 Aug 2011 22:31:11 -0300 Subject: [PATCH 0732/3747] Add runserver.py filename to match earlier section of example --- docs/patterns/packages.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 4cd9dd27..79fd2c58 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -77,6 +77,7 @@ And this is what `views.py` would look like:: You should then end up with something like that:: /yourapplication + /runserver.py /yourapplication /__init__.py /views.py From 7155f11a723695352323d484610b9a1c6798eb69 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 12:35:41 +0200 Subject: [PATCH 0733/3747] Added HTTP exception trapping. This should fix #294 --- CHANGES | 2 ++ docs/config.rst | 20 ++++++++++++++++++++ flask/app.py | 36 +++++++++++++++++++++++++++++------- tests/flask_tests.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index 4f400d8e..ae9cbdb9 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,8 @@ Relase date to be decided, codename to be chosen. - Empty session cookies are now deleted properly automatically. - View functions can now opt out of getting the automatic OPTIONS implementation. +- HTTP exceptions and Bad Request Key Errors can now be trapped so that they + show up normally in the traceback. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index 2487e8b7..4995ee84 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -81,6 +81,23 @@ The following configuration values are used internally by Flask: reject incoming requests with a content length greater than this by returning a 413 status code. +``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will + not execute the error handlers of HTTP + exceptions but instead treat the + exception like any other and bubble it + through the exception stack. This is + helpful for hairy debugging situations + where you have to find out where an HTTP + exception is coming from. +``TRAP_BAD_REQUEST_KEY_ERRORS`` Werkzeug's internal data structures that + deal with request specific data will + raise special key errors that are also + bad request exceptions. By default + these will be converted into 400 + responses which however can make + debugging some issues harder. If this + config is set to ``True`` you will get + a regular traceback instead. ================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -114,6 +131,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.7 ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` +.. versionadded:: 0.8 + ``TRAP_BAD_REQUEST_KEY_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 5c448144..359a233d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,7 @@ from itertools import chain from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed + MethodNotAllowed, BadRequest from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ locked_cached_property, _tojson_filter, _endpoint_from_view_func @@ -197,7 +197,9 @@ class Flask(_PackageBoundObject): 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, - 'MAX_CONTENT_LENGTH': None + 'MAX_CONTENT_LENGTH': None, + 'TRAP_BAD_REQUEST_KEY_ERRORS': False, + 'TRAP_HTTP_EXCEPTIONS': False }) #: The rule object to use for URL rules created. This is used by @@ -983,6 +985,24 @@ class Flask(_PackageBoundObject): return e return handler(e) + def trap_http_exception(self, e): + """Checks if an HTTP exception should be trapped or not. By default + this will return `False` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_KEY_ERRORS`` is set to `True`. It + also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. + + This is called for all HTTP exceptions raised by a view function. + If it returns `True` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + """ + if self.config['TRAP_HTTP_EXCEPTIONS']: + return True + if self.config['TRAP_BAD_REQUEST_KEY_ERRORS']: + return isinstance(e, BadRequest) and isinstance(e, LookupError) + return False + def handle_user_exception(self, e): """This method is called whenever an exception occurs that should be handled. A special case are @@ -993,14 +1013,16 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - # ensure not to trash sys.exc_info() at that point in case someone - # wants the traceback preserved in handle_http_exception. - if isinstance(e, HTTPException): - return self.handle_http_exception(e) - exc_type, exc_value, tb = sys.exc_info() assert exc_value is e + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. Of course + # we cannot prevent users from trashing it themselves in a custom + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + blueprint_handlers = () handlers = self.error_handler_spec.get(request.blueprint) if handlers is not None: diff --git a/tests/flask_tests.py b/tests/flask_tests.py index fb3e0e52..dbd38e12 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -23,7 +23,7 @@ from contextlib import contextmanager from functools import update_wrapper from datetime import datetime from werkzeug import parse_date, parse_options_header -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import NotFound, BadRequest from werkzeug.http import parse_set_header from jinja2 import TemplateNotFound from cStringIO import StringIO @@ -592,6 +592,40 @@ class BasicFunctionalityTestCase(unittest.TestCase): c = app.test_client() assert c.get('/').data == '42' + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + assert c.get('/fail').status_code == 400 + + app.config['TRAP_BAD_REQUEST_KEY_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + assert isinstance(e, BadRequest) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + def test_teardown_on_pop(self): buffer = [] app = flask.Flask(__name__) From afe5d3cbd44056893aa8c11f4bf17496cee92022 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 12:37:57 +0200 Subject: [PATCH 0734/3747] Added a missing versionadded --- flask/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/app.py b/flask/app.py index 359a233d..1eed507d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -996,6 +996,8 @@ class Flask(_PackageBoundObject): exception is not called and it shows up as regular exception in the traceback. This is helpful for debugging implicitly raised HTTP exceptions. + + .. versionadded:: 0.8 """ if self.config['TRAP_HTTP_EXCEPTIONS']: return True From a69b437af705dc8fa4e1b38b86ddad77d6dedb54 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 12:40:44 +0200 Subject: [PATCH 0735/3747] Cleanup for the json property --- flask/wrappers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 37d972d1..57c62ce1 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -96,11 +96,8 @@ class Request(RequestBase): if self.mimetype == 'application/json': request_charset = self.mimetype_params.get('charset') if request_charset is not None: - j = json.loads(self.data, encoding=request_charset ) - else: - j = json.loads(self.data) - - return j + return json.loads(self.data, encoding=request_charset) + return json.loads(self.data) class Response(ResponseBase): From 5f367e879db5e887f68d2c026fe25a0c62cc3c33 Mon Sep 17 00:00:00 2001 From: Brandon Stafford Date: Fri, 5 Aug 2011 11:12:21 -0300 Subject: [PATCH 0736/3747] Fix typos. --- docs/blueprints.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index c71d11bb..9422fd02 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -125,16 +125,16 @@ Blueprint Resource Folder ````````````````````````` Like for regular applications, blueprints are considered to be contained -in a folder. While multiple blueprints can origin from the same folder, +in a folder. While multiple blueprints can originate from the same folder, it does not have to be the case and it's usually not recommended. -The folder is infered from the second argument to :class:`Blueprint` which -is ususally `__name__`. This argument specifies what logical Python +The folder is inferred from the second argument to :class:`Blueprint` which +is usually `__name__`. This argument specifies what logical Python module or package corresponds to the blueprint. If it points to an actual Python package that package (which is a folder on the filesystem) is the resource folder. If it's a module, the package the module is contained in will be the resource folder. You can access the -:attr:`Blueprint.root_path` property to see what's the resource folder:: +:attr:`Blueprint.root_path` property to see what the resource folder is:: >>> simple_page.root_path '/Users/username/TestProject/yourapplication' From 2e022cb2727cfa99355261f2c18d1e0fc56e82ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 16:43:42 +0200 Subject: [PATCH 0737/3747] Added debughelpers. Flask will now tell you if you forget enctype --- CHANGES | 2 ++ flask/debughelpers.py | 52 +++++++++++++++++++++++++++++++++++++++++++ flask/wrappers.py | 11 +++++++++ 3 files changed, 65 insertions(+) create mode 100644 flask/debughelpers.py diff --git a/CHANGES b/CHANGES index ae9cbdb9..b7be2c52 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Relase date to be decided, codename to be chosen. OPTIONS implementation. - HTTP exceptions and Bad Request Key Errors can now be trapped so that they show up normally in the traceback. +- Flask in debug mode is now detecting some common problems and tries to + warn you about them. Version 0.7.3 ------------- diff --git a/flask/debughelpers.py b/flask/debughelpers.py new file mode 100644 index 00000000..dd1fffe9 --- /dev/null +++ b/flask/debughelpers.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" + flask.debughelpers + ~~~~~~~~~~~~~~~~~~ + + Various helpers to make the development experience better. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request, key): + form_matches = request.form.getlist(key) + buf = ['You tried to access the file "%s" in the request.files ' + 'dictionary but it does not exist. The mimetype for the request ' + 'is "%s" instead of "multipart/form-data" which means that no ' + 'files were transmitted. To fix this error you most likely have ' + 'to provide enctype="multipart/form-data" in your form.' % + (key, request.mimetype)] + if form_matches: + buf.append('\n\nThe browser instead most likely submitted the ' + 'filenames in the form. This was submitted: %s' % + ', '.join('"%s"' % x for x in form_matches)) + self.msg = ''.join(buf) + + def __str__(self): + return self.msg + + +def make_enctype_error_multidict(request): + """Since Flask 0.8 we're monkeypatching the files object in case a + request is detected that does not use multipart form data but the files + object is accessed. + """ + oldcls = request.files.__class__ + class newcls(oldcls): + def __getitem__(self, key): + try: + return oldcls.__getitem__(self, key) + except KeyError, e: + if key not in request.form: + raise + raise DebugFilesKeyError(request, key) + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls diff --git a/flask/wrappers.py b/flask/wrappers.py index 57c62ce1..06f60beb 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,6 +12,7 @@ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.utils import cached_property +from .debughelpers import make_enctype_error_multidict from .helpers import json, _assert_have_json from .globals import _request_ctx_stack @@ -99,6 +100,16 @@ class Request(RequestBase): return json.loads(self.data, encoding=request_charset) return json.loads(self.data) + def _load_form_data(self): + RequestBase._load_form_data(self) + + # in debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + ctx = _request_ctx_stack.top + if ctx is not None and ctx.app.debug and \ + self.mimetype != 'multipart/form-data': + make_enctype_error_multidict(self) + class Response(ResponseBase): """The response object that is used by default in Flask. Works like the From 17a836d49c8d496d50b726132e58749cf2741dd6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 16:47:12 +0200 Subject: [PATCH 0738/3747] Better wording --- flask/debughelpers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index dd1fffe9..9ba81e5e 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -20,13 +20,13 @@ class DebugFilesKeyError(KeyError, AssertionError): buf = ['You tried to access the file "%s" in the request.files ' 'dictionary but it does not exist. The mimetype for the request ' 'is "%s" instead of "multipart/form-data" which means that no ' - 'files were transmitted. To fix this error you most likely have ' - 'to provide enctype="multipart/form-data" in your form.' % + 'file contents were transmitted. To fix this error you should ' + 'provide enctype="multipart/form-data" in your form.' % (key, request.mimetype)] if form_matches: - buf.append('\n\nThe browser instead most likely submitted the ' - 'filenames in the form. This was submitted: %s' % - ', '.join('"%s"' % x for x in form_matches)) + buf.append('\n\nThe browser instead some file names. This was ' + 'submitted: %s' % ', '.join('"%s"' % x + for x in form_matches)) self.msg = ''.join(buf) def __str__(self): From 23c1dd8db2a8dc028516a57b6dca47733bb82173 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 16:50:02 +0200 Subject: [PATCH 0739/3747] Wording fail --- flask/debughelpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 9ba81e5e..8f167e7e 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -24,8 +24,8 @@ class DebugFilesKeyError(KeyError, AssertionError): 'provide enctype="multipart/form-data" in your form.' % (key, request.mimetype)] if form_matches: - buf.append('\n\nThe browser instead some file names. This was ' - 'submitted: %s' % ', '.join('"%s"' % x + buf.append('\n\nThe browser instead transmitted some file names. ' + 'This was submitted: %s' % ', '.join('"%s"' % x for x in form_matches)) self.msg = ''.join(buf) From 3400b78db23fbd305cb11368946be1bf280e2696 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 16:51:45 +0200 Subject: [PATCH 0740/3747] Only provide that monkeypatch if all files are missing --- flask/wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 06f60beb..8ea4b840 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -107,7 +107,7 @@ class Request(RequestBase): # subclass that raises a different error for key errors. ctx = _request_ctx_stack.top if ctx is not None and ctx.app.debug and \ - self.mimetype != 'multipart/form-data': + self.mimetype != 'multipart/form-data' and not self.files: make_enctype_error_multidict(self) From f3db68c8cee4f48fbc078e1f5a73bb025b425f91 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 16:56:43 +0200 Subject: [PATCH 0741/3747] Added testcase for the debug behavior and explicit encoding --- flask/debughelpers.py | 2 +- tests/flask_tests.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 8f167e7e..e3bdf76f 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -27,7 +27,7 @@ class DebugFilesKeyError(KeyError, AssertionError): buf.append('\n\nThe browser instead transmitted some file names. ' 'This was submitted: %s' % ', '.join('"%s"' % x for x in form_matches)) - self.msg = ''.join(buf) + self.msg = ''.join(buf).encode('utf-8') def __str__(self): return self.msg diff --git a/tests/flask_tests.py b/tests/flask_tests.py index dbd38e12..3789cd97 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -626,6 +626,26 @@ class BasicFunctionalityTestCase(unittest.TestCase): else: self.fail('Expected exception') + def test_enctype_debug_helper(self): + from flask.debughelpers import DebugFilesKeyError + app = flask.Flask(__name__) + app.debug = True + @app.route('/fail', methods=['POST']) + def index(): + return flask.request.files['foo'].filename + + # with statement is important because we leave an exception on the + # stack otherwise and we want to ensure that this is not the case + # to not negatively affect other tests. + with app.test_client() as c: + try: + c.post('/fail', data={'foo': 'index.txt'}) + except DebugFilesKeyError, e: + assert 'no file contents were transmitted' in str(e) + assert 'This was submitted: "index.txt"' in str(e) + else: + self.fail('Expected exception') + def test_teardown_on_pop(self): buffer = [] app = flask.Flask(__name__) From ac99cd33a827f0694d718dae5f2699430263ed7e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 5 Aug 2011 21:54:12 +0200 Subject: [PATCH 0742/3747] Better internal method name --- flask/debughelpers.py | 2 +- flask/wrappers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index e3bdf76f..7eae61c5 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -33,7 +33,7 @@ class DebugFilesKeyError(KeyError, AssertionError): return self.msg -def make_enctype_error_multidict(request): +def attach_enctype_error_multidict(request): """Since Flask 0.8 we're monkeypatching the files object in case a request is detected that does not use multipart form data but the files object is accessed. diff --git a/flask/wrappers.py b/flask/wrappers.py index 8ea4b840..73169799 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -12,7 +12,7 @@ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.utils import cached_property -from .debughelpers import make_enctype_error_multidict +from .debughelpers import attach_enctype_error_multidict from .helpers import json, _assert_have_json from .globals import _request_ctx_stack @@ -108,7 +108,7 @@ class Request(RequestBase): ctx = _request_ctx_stack.top if ctx is not None and ctx.app.debug and \ self.mimetype != 'multipart/form-data' and not self.files: - make_enctype_error_multidict(self) + attach_enctype_error_multidict(self) class Response(ResponseBase): From 5ca17c86c088eb79a84b97e913e3122d917d042f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 01:51:02 +0200 Subject: [PATCH 0743/3747] Added a missing colon --- docs/patterns/sqlite3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index d0ec5a27..bc471f66 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -34,7 +34,7 @@ executed the before-request handlers for you. If you are attempting to use the database from a script or the interactive Python shell you would have to do something like this:: - with app.test_request_context() + with app.test_request_context(): app.preprocess_request() # now you can use the g.db object From 5500986971b28f270a27db633acf19984eee609e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 02:30:34 +0200 Subject: [PATCH 0744/3747] Flask in debug mode will now complain if views are attached after the first view was handled. --- CHANGES | 3 +++ flask/app.py | 35 +++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 22 ++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/CHANGES b/CHANGES index b7be2c52..e4300363 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,9 @@ Relase date to be decided, codename to be chosen. show up normally in the traceback. - Flask in debug mode is now detecting some common problems and tries to warn you about them. +- Flask in debug mode will now complain with an assertion error if a view + was attached after the first request was handled. This gives earlier + feedback when users forget to import view code ahead of time. Version 0.7.3 ------------- diff --git a/flask/app.py b/flask/app.py index 1eed507d..df147863 100644 --- a/flask/app.py +++ b/flask/app.py @@ -15,6 +15,7 @@ import sys from threading import Lock from datetime import timedelta from itertools import chain +from functools import update_wrapper from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule @@ -38,6 +39,23 @@ from .signals import request_started, request_finished, got_request_exception, \ _logger_lock = Lock() +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + def wrapper_func(self, *args, **kwargs): + if self.debug and self._got_first_request: + raise AssertionError('A setup function was called after the ' + 'first request was handled. This usually indicates a bug ' + 'in the application where a module was not imported ' + 'and decorators or other functionality was called too late.\n' + 'To fix this make sure to import all your view modules, ' + 'database models and everything related at a central place ' + 'before the application starts serving requests.') + return f(self, *args, **kwargs) + return update_wrapper(wrapper_func, f) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -365,6 +383,10 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + # register the static folder for the application. Do that even # if the folder does not exist. First of all it might be created # while the server is running (usually happens during development) @@ -642,6 +664,7 @@ class Flask(_PackageBoundObject): self.register_blueprint(module, **options) + @setupmethod def register_blueprint(self, blueprint, **options): """Registers a blueprint on the application. @@ -659,6 +682,7 @@ class Flask(_PackageBoundObject): first_registration = True blueprint.register(self, options, first_registration) + @setupmethod def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the @@ -812,6 +836,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def endpoint(self, endpoint): """A decorator to register a function as an endpoint. Example:: @@ -827,6 +852,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def errorhandler(self, code_or_exception): """A decorator that is used to register a function give a given error code. Example:: @@ -877,6 +903,7 @@ class Flask(_PackageBoundObject): """ self._register_error_handler(None, code_or_exception, f) + @setupmethod def _register_error_handler(self, key, code_or_exception, f): if isinstance(code_or_exception, HTTPException): code_or_exception = code_or_exception.code @@ -889,6 +916,7 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .append((code_or_exception, f)) + @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function @@ -906,11 +934,13 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" self.before_request_funcs.setdefault(None, []).append(f) return f + @setupmethod def after_request(self, f): """Register a function to be run after each request. Your function must take one parameter, a :attr:`response_class` object and return @@ -922,6 +952,7 @@ class Flask(_PackageBoundObject): self.after_request_funcs.setdefault(None, []).append(f) return f + @setupmethod def teardown_request(self, f): """Register a function to be run at the end of each request, regardless of whether there was an exception or not. These functions @@ -948,11 +979,13 @@ class Flask(_PackageBoundObject): self.teardown_request_funcs.setdefault(None, []).append(f) return f + @setupmethod def context_processor(self, f): """Registers a template context processor function.""" self.template_context_processors[None].append(f) return f + @setupmethod def url_value_preprocessor(self, f): """Registers a function as URL value preprocessor for all view functions of the application. It's called before the view functions @@ -961,6 +994,7 @@ class Flask(_PackageBoundObject): self.url_value_preprocessors.setdefault(None, []).append(f) return f + @setupmethod def url_defaults(self, f): """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should @@ -1097,6 +1131,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ + self._got_first_request = True try: request_started.send(self) rv = self.preprocess_request() diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3789cd97..59149286 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -944,6 +944,28 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(c.get('/de/about').data, '/foo') self.assertEqual(c.get('/foo').data, '/en/about') + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assertEqual(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assertEqual(app.test_client().get('/foo').data, 'Meh') + class JSONTestCase(unittest.TestCase): From 02a131746074203dc8d747308f4d2c7ece8aa743 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 12:43:38 +0200 Subject: [PATCH 0745/3747] Added the ability to trigger functions before the first request to the application --- CHANGES | 2 ++ flask/app.py | 52 ++++++++++++++++++++++++++++++++++++++++++-- flask/blueprints.py | 7 ++++++ tests/flask_tests.py | 15 +++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index e4300363..cc8198d2 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,8 @@ Relase date to be decided, codename to be chosen. - Flask in debug mode will now complain with an assertion error if a view was attached after the first request was handled. This gives earlier feedback when users forget to import view code ahead of time. +- Added the ability to register callbacks that are only triggered once at + the beginning of the first request. (:meth:`Flask.before_first_request`) Version 0.7.3 ------------- diff --git a/flask/app.py b/flask/app.py index df147863..d299b4fb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -291,6 +291,13 @@ class Flask(_PackageBoundObject): #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} + #: A lists of functions that should be called at the beginning of the + #: first request to this instance. To register a function here, use + #: the :meth:`before_first_request` decorator. + #: + #: .. versionadded:: 0.8 + self.before_first_request_funcs = [] + #: A dictionary with lists of functions that should be called after #: each request. The key of the dictionary is the name of the blueprint #: this function is active for, `None` for all requests. This can for @@ -386,6 +393,7 @@ class Flask(_PackageBoundObject): # tracks internally if the application already handled at least one # request. self._got_first_request = False + self._before_request_lock = Lock() # register the static folder for the application. Do that even # if the folder does not exist. First of all it might be created @@ -474,6 +482,15 @@ class Flask(_PackageBoundObject): return rv + @property + def got_first_request(self): + """This attribute is set to `True` if the application started + handling the first request. + + .. versionadded:: 0.8 + """ + return self._got_first_request + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` and :meth:`select_jinja_autoescape`. Since 0.7 this also adds @@ -581,7 +598,13 @@ class Flask(_PackageBoundObject): self.debug = options.pop('debug') options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) - return run_simple(host, port, self, **options) + try: + run_simple(host, port, self, **options) + finally: + # reset the first request information if the development server + # resetted normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False def test_client(self, use_cookies=True): """Creates a test client for this application. For information @@ -940,6 +963,15 @@ class Flask(_PackageBoundObject): self.before_request_funcs.setdefault(None, []).append(f) return f + @setupmethod + def before_first_request(self, f): + """Registers a function to be run before the first request to this + instance of the application. + + .. versionadded:: 0.8 + """ + self.before_first_request_funcs.append(f) + @setupmethod def after_request(self, f): """Register a function to be run after each request. Your function @@ -1131,7 +1163,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - self._got_first_request = True + self.try_trigger_before_first_request_functions() try: request_started.send(self) rv = self.preprocess_request() @@ -1144,6 +1176,22 @@ class Flask(_PackageBoundObject): request_finished.send(self, response=response) return response + def try_trigger_before_first_request_functions(self): + """Called before each request and will ensure that it triggers + the :attr:`before_first_request_funcs` and only exactly once per + application instance (which means process usually). + + .. versionadded:: 0.8 + """ + if self._got_first_request: + return + with self._before_request_lock: + if self._got_first_request: + return + self._got_first_request = True + for func in self.before_first_request_funcs: + func() + def make_default_options_response(self): """This method is called to create the default `OPTIONS` response. This can be changed through subclassing to change the default diff --git a/flask/blueprints.py b/flask/blueprints.py index 0c3a7d0f..075961ab 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -199,6 +199,13 @@ class Blueprint(_PackageBoundObject): .setdefault(None, []).append(f)) return f + def before_app_first_request(self, f): + """Like :meth:`Flask.before_first_request`. Such a function is + executed before the first request to the application. + """ + self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) + return f + def after_request(self, f): """Like :meth:`Flask.after_request` but for a blueprint. This function is only executed after each request that is handled by a function of diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 59149286..467381eb 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -950,6 +950,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): @app.route('/') def index(): return 'Awesome' + self.assert_(not app.got_first_request) self.assertEqual(app.test_client().get('/').data, 'Awesome') try: @app.route('/foo') @@ -965,6 +966,20 @@ class BasicFunctionalityTestCase(unittest.TestCase): def working(): return 'Meh' self.assertEqual(app.test_client().get('/foo').data, 'Meh') + self.assert_(app.got_first_request) + + def test_before_first_request_functions(self): + got = [] + app = flask.Flask(__name__) + @app.before_first_request + def foo(): + got.append(42) + c = app.test_client() + c.get('/') + self.assertEqual(got, [42]) + c.get('/') + self.assertEqual(got, [42]) + self.assert_(app.got_first_request) class JSONTestCase(unittest.TestCase): From ae00f6d149c353e1ecb683ccbf01be221324a646 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 14:15:33 +0200 Subject: [PATCH 0746/3747] Tweaked autodoc to skip :internal: methods. --- docs/conf.py | 4 +++- docs/flaskdocext.py | 16 ++++++++++++++++ flask/app.py | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 docs/flaskdocext.py diff --git a/docs/conf.py b/docs/conf.py index 390361a6..16d7e670 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ import sys, os # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.abspath('_themes')) +sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- @@ -25,7 +26,8 @@ sys.path.append(os.path.abspath('_themes')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'flaskdocext'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/flaskdocext.py b/docs/flaskdocext.py new file mode 100644 index 00000000..db4cfd20 --- /dev/null +++ b/docs/flaskdocext.py @@ -0,0 +1,16 @@ +import re +import inspect + + +_internal_mark_re = re.compile(r'^\s*:internal:\s*$(?m)') + + +def skip_member(app, what, name, obj, skip, options): + docstring = inspect.getdoc(obj) + if skip: + return True + return _internal_mark_re.search(docstring or '') is not None + + +def setup(app): + app.connect('autodoc-skip-member', skip_member) diff --git a/flask/app.py b/flask/app.py index d299b4fb..8995c511 100644 --- a/flask/app.py +++ b/flask/app.py @@ -257,7 +257,7 @@ class Flask(_PackageBoundObject): #: to load a config from files. self.config = Config(self.root_path, self.default_config) - #: Prepare the deferred setup of the logger. + # Prepare the deferred setup of the logger. self._logger = None self.logger_name = self.import_name @@ -1181,7 +1181,7 @@ class Flask(_PackageBoundObject): the :attr:`before_first_request_funcs` and only exactly once per application instance (which means process usually). - .. versionadded:: 0.8 + :internal: """ if self._got_first_request: return From 13005d76b98979eeacb169765df3cc2a00b0bb4d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 15:23:57 +0200 Subject: [PATCH 0747/3747] Improved cookie documentation --- docs/quickstart.rst | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index baca5050..8bc544b0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -588,11 +588,38 @@ For some better examples, checkout the :ref:`uploading-files` pattern. Cookies ``````` -To access cookies you can use the :attr:`~flask.request.cookies` -attribute. Again this is a dictionary with all the cookies the client -transmits. If you want to use sessions, do not use the cookies directly -but instead use the :ref:`sessions` in Flask that add some security on top -of cookies for you. +To access cookies you can use the :attr:`~flask.Request.cookies` +attribute. To set cookies you can use the +:attr:`~flask.Response.set_cookie` method of response objects. The +:attr:`~flask.Request.cookies` attribute of request objects is a +dictionary with all the cookies the client transmits. If you want to use +sessions, do not use the cookies directly but instead use the +:ref:`sessions` in Flask that add some security on top of cookies for you. + +Reading cookies:: + + from flask import request + + @app.route('/') + def index(): + username = request.cookies.get('username') + # use cookies.get(key) instead of cookies[key] to not get a + # KeyError if the cookie is missing. + +Storing cookies:: + + from flask import make_response + + @app.route('/') + def index(): + resp = make_response(render_template(...)) + resp.set_cookie('username', 'the username') + return resp + +Note that cookies are set on response objects. Since you normally you +just return strings from the view functions Flask will convert them into +response objects for you. If you explicitly want to do that you can use +the :meth:`~flask.make_response` function and then modify it. Redirects and Errors From 5b1653200047c7c257c924b66cf77f85247921a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Aug 2011 16:21:58 +0200 Subject: [PATCH 0748/3747] Extended quickstart docs for the headers --- docs/quickstart.rst | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8bc544b0..690433db 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -621,6 +621,7 @@ just return strings from the view functions Flask will convert them into response objects for you. If you explicitly want to do that you can use the :meth:`~flask.make_response` function and then modify it. +For this also see :ref:`about-responses`. Redirects and Errors -------------------- @@ -658,6 +659,49 @@ Note the ``404`` after the :func:`~flask.render_template` call. This tells Flask that the status code of that page should be 404 which means not found. By default 200 is assumed which translates to: all went well. +.. _about-responses: + +About Responses +--------------- + +The return value from a view function is automatically converted into a +response object for you. If the return value is a string it's converted +into a response object with the string as response body, an ``200 OK`` +error code and a ``text/html`` mimetype. The logic that Flask applies to +converting return values into response objects is as follows: + +1. If a response object of the correct type is returned it's directly + returned from the view. +2. If it's a string, a response object is created with that data and the + default parameters. +3. If a tuple is returned the response object is created by passing the + tuple as arguments to the response object's constructor. +4. If neither of that works, Flask will assume the return value is a + valid WSGI application and converts that into a response object. + +If you want to get hold of the resulting response object inside the view +you can use the :func:`~flask.make_response` function. + +Imagine you have a view like this: + +.. sourcecode:: python + + @app.errorhandler(404) + def not_found(error): + return render_template('error.html'), 404 + +You just need to wrap the return expression with +:func:`~flask.make_response` and get the result object to modify it, then +return it: + +.. sourcecode:: python + + @app.errorhandler(404) + def not_found(error): + resp = make_response(render_template('error.html'), 404) + resp.headers['X-Something'] = 'A value' + return resp + .. _sessions: Sessions From 505cd663cd75b5593a1c9db0eded5298bd67139a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 8 Aug 2011 01:26:39 +0200 Subject: [PATCH 0749/3747] Hopefully fixed release script date parsing thingy. This fixes #299 --- scripts/make-release.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/make-release.py b/scripts/make-release.py index 574cb75d..31f4fa9e 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -16,6 +16,8 @@ import re from datetime import datetime, date from subprocess import Popen, PIPE +_date_clean_re = re.compile(r'(\d+)(st|nd|rd|th)') + def parse_changelog(): with open('CHANGES') as f: @@ -52,8 +54,7 @@ def bump_version(version): def parse_date(string): - string = string.replace('th ', ' ').replace('nd ', ' ') \ - .replace('rd ', ' ').replace('st ', ' ') + string = _date_clean_re.sub(r'\1', string) return datetime.strptime(string, '%B %d %Y') From acac64e36a046cebae07bee7f3e8106b4a80288a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 8 Aug 2011 21:46:53 +0200 Subject: [PATCH 0750/3747] Don't only catch BadRequest key errors but all bad request errors. --- docs/config.rst | 14 ++++++++------ flask/app.py | 8 ++++---- tests/flask_tests.py | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 4995ee84..f50e3180 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -89,13 +89,15 @@ The following configuration values are used internally by Flask: helpful for hairy debugging situations where you have to find out where an HTTP exception is coming from. -``TRAP_BAD_REQUEST_KEY_ERRORS`` Werkzeug's internal data structures that +``TRAP_BAD_REQUEST_ERRORS`` Werkzeug's internal data structures that deal with request specific data will raise special key errors that are also - bad request exceptions. By default - these will be converted into 400 - responses which however can make - debugging some issues harder. If this + bad request exceptions. Likewise many + operations can implicitly fail with a + BadRequest exception for consistency. + Since it's nice for debugging to know + why exactly it failed this flag can be + used to debug those situations. If this config is set to ``True`` you will get a regular traceback instead. ================================= ========================================= @@ -132,7 +134,7 @@ The following configuration values are used internally by Flask: ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` .. versionadded:: 0.8 - ``TRAP_BAD_REQUEST_KEY_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 8995c511..009841f8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -216,7 +216,7 @@ class Flask(_PackageBoundObject): 'LOGGER_NAME': None, 'SERVER_NAME': None, 'MAX_CONTENT_LENGTH': None, - 'TRAP_BAD_REQUEST_KEY_ERRORS': False, + 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False }) @@ -1054,7 +1054,7 @@ class Flask(_PackageBoundObject): def trap_http_exception(self, e): """Checks if an HTTP exception should be trapped or not. By default this will return `False` for all exceptions except for a bad request - key error if ``TRAP_BAD_REQUEST_KEY_ERRORS`` is set to `True`. It + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to `True`. It also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. This is called for all HTTP exceptions raised by a view function. @@ -1067,8 +1067,8 @@ class Flask(_PackageBoundObject): """ if self.config['TRAP_HTTP_EXCEPTIONS']: return True - if self.config['TRAP_BAD_REQUEST_KEY_ERRORS']: - return isinstance(e, BadRequest) and isinstance(e, LookupError) + if self.config['TRAP_BAD_REQUEST_ERRORS']: + return isinstance(e, BadRequest) return False def handle_user_exception(self, e): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 467381eb..5e592710 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -601,7 +601,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): c = app.test_client() assert c.get('/fail').status_code == 400 - app.config['TRAP_BAD_REQUEST_KEY_ERRORS'] = True + app.config['TRAP_BAD_REQUEST_ERRORS'] = True c = app.test_client() try: c.get('/fail') From ce701319756abef4d09c585828a630b8fdef89fc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 8 Aug 2011 21:47:26 +0200 Subject: [PATCH 0751/3747] If JSON parsing fails it now issues a BadRequest exception. --- CHANGES | 5 ++++- docs/upgrading.rst | 7 +++++++ flask/wrappers.py | 19 ++++++++++++++++--- tests/flask_tests.py | 9 +++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index cc8198d2..c1d33869 100644 --- a/CHANGES +++ b/CHANGES @@ -14,7 +14,7 @@ Relase date to be decided, codename to be chosen. - Empty session cookies are now deleted properly automatically. - View functions can now opt out of getting the automatic OPTIONS implementation. -- HTTP exceptions and Bad Request Key Errors can now be trapped so that they +- HTTP exceptions and Bad Request errors can now be trapped so that they show up normally in the traceback. - Flask in debug mode is now detecting some common problems and tries to warn you about them. @@ -23,6 +23,9 @@ Relase date to be decided, codename to be chosen. feedback when users forget to import view code ahead of time. - Added the ability to register callbacks that are only triggered once at the beginning of the first request. (:meth:`Flask.before_first_request`) +- Malformed JSON data will now trigger a bad request HTTP exception instead + of a value error which usually would result in a 500 internal server + error if not handled. This is a backwards incompatible change. Version 0.7.3 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 154b51a7..13b5be71 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -29,6 +29,13 @@ object. With that introduction we moved the implementation details for the session system into a new module called :mod:`flask.sessions`. If you used the previously undocumented session support we urge you to upgrade. +If invalid JSON data was submitted Flask will now raise a +:exc:`~werkzeug.exceptions.BadRequest` exception instead of letting the +default :exc:`ValueError` bubble up. This has the advantage that you no +longer have to handle that error to avoid an internal server error showing +up for the user. If you were catching this down explicitly in the past +as `ValueError` you will need to change this. + Version 0.7 ----------- diff --git a/flask/wrappers.py b/flask/wrappers.py index 73169799..2d3d53f7 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -10,6 +10,7 @@ """ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase +from werkzeug.exceptions import BadRequest from werkzeug.utils import cached_property from .debughelpers import attach_enctype_error_multidict @@ -96,9 +97,21 @@ class Request(RequestBase): _assert_have_json() if self.mimetype == 'application/json': request_charset = self.mimetype_params.get('charset') - if request_charset is not None: - return json.loads(self.data, encoding=request_charset) - return json.loads(self.data) + try: + if request_charset is not None: + return json.loads(self.data, encoding=request_charset) + return json.loads(self.data) + except ValueError, e: + return self.on_json_loading_failed(e) + + def on_json_loading_failed(self, e): + """Called if decoding of the JSON data failed. The return value of + this method is used by :attr:`json` when an error ocurred. The + default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + + .. versionadded:: 0.8 + """ + raise BadRequest() def _load_form_data(self): RequestBase._load_form_data(self) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 5e592710..7c1f4b2a 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -984,6 +984,15 @@ class BasicFunctionalityTestCase(unittest.TestCase): class JSONTestCase(unittest.TestCase): + def test_json_bad_requests(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assertEqual(rv.status_code, 400) + def test_json_body_encoding(self): app = flask.Flask(__name__) app.testing = True From 68473291344629dff3955fdc07776f2fca7b9b69 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 9 Aug 2011 14:51:06 +0200 Subject: [PATCH 0752/3747] Flask will now give you an error in debug mode if a post request caused a redirect by the routing system. --- flask/app.py | 20 ++++++++++++++++++-- flask/debughelpers.py | 26 ++++++++++++++++++++++++++ tests/flask_tests.py | 24 ++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 009841f8..00c36a38 100644 --- a/flask/app.py +++ b/flask/app.py @@ -18,7 +18,7 @@ from itertools import chain from functools import update_wrapper from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule +from werkzeug.routing import Map, Rule, RequestRedirect from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed, BadRequest @@ -1134,6 +1134,22 @@ class Flask(_PackageBoundObject): return InternalServerError() return handler(e) + def raise_routing_exception(self, request): + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if not self.debug \ + or not isinstance(request.routing_exception, RequestRedirect) \ + or request.method in ('GET', 'HEAD', 'OPTIONS'): + raise request.routing_exception + + from .debughelpers import FormDataRoutingRedirect + raise FormDataRoutingRedirect(request) + def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to @@ -1146,7 +1162,7 @@ class Flask(_PackageBoundObject): """ req = _request_ctx_stack.top.request if req.routing_exception is not None: - raise req.routing_exception + self.raise_routing_exception(req) rule = req.url_rule # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 7eae61c5..b4f73dd3 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -33,6 +33,32 @@ class DebugFilesKeyError(KeyError, AssertionError): return self.msg +class FormDataRoutingRedirect(AssertionError): + """This exception is raised by Flask in debug mode if it detects a + redirect caused by the routing system when the request method is not + GET, HEAD or OPTIONS. Reasoning: form data will be dropped. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = ['A request was sent to this URL (%s) but a redirect was ' + 'issued automatically by the routing system to "%s".' + % (request.url, exc.new_url)] + + # In case just a slash was appended we can be extra helpful + if request.base_url + '/' == exc.new_url.split('?')[0]: + buf.append(' The URL was defined with a trailing slash so ' + 'Flask will automatically redirect to the URL ' + 'with the trailing slash if it was accessed ' + 'without one.') + + buf.append(' Make sure to directly send your %s-request to this URL ' + 'since we can\'t make browsers or HTTP clients redirect ' + 'with form data.' % request.method) + buf.append('\n\nNote: this exception is only raised in debug mode') + AssertionError.__init__(self, ''.join(buf).encode('utf-8')) + + def attach_enctype_error_multidict(request): """Since Flask 0.8 we're monkeypatching the files object in case a request is detected that does not use multipart form data but the files diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7c1f4b2a..e5fbf20f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -981,6 +981,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(got, [42]) self.assert_(app.got_first_request) + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assertEqual(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assertEqual(rv.data, 'success') + class JSONTestCase(unittest.TestCase): From 92c7bdd2dd943b173476a6d2f26e0831579e2d3d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 9 Aug 2011 15:16:41 +0200 Subject: [PATCH 0753/3747] Documented Request.headers --- docs/api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index ff7e597a..e657bf6e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -59,6 +59,10 @@ Incoming Request Data of the time it is a better idea to use :attr:`data` which will give you that data as a string. The stream only returns the data once. + .. attribute:: headers + + The incoming request headers as a dictionary like object. + .. attribute:: data Contains the incoming request data as string in case it came with From 63876614b6734e6404f17c9d3d22b71b3f43d158 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 9 Aug 2011 16:16:04 +0200 Subject: [PATCH 0754/3747] Removed second mention of json. This fixes #290 --- docs/api.rst | 6 ------ flask/wrappers.py | 4 +++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e657bf6e..6b695bfa 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -119,12 +119,6 @@ Incoming Request Data Libraries that do that are prototype, jQuery and Mochikit and probably some more. - .. attribute:: json - - Contains the parsed body of the JSON request if the mimetype of - the incoming data was `application/json`. This requires Python 2.6 - or an installed version of simplejson. - .. class:: request To access incoming request data, you can use the global `request` diff --git a/flask/wrappers.py b/flask/wrappers.py index 2d3d53f7..aed0a8d0 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -91,7 +91,9 @@ class Request(RequestBase): @cached_property def json(self): """If the mimetype is `application/json` this will contain the - parsed JSON data. + parsed JSON data. Otherwise this will be `None`. + + This requires Python 2.6 or an installed version of simplejson. """ if __debug__: _assert_have_json() From 153ecbc9202f249c4037602115ed2c2b5785550d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 13:34:58 +0200 Subject: [PATCH 0755/3747] Implemented instance paths --- CHANGES | 5 +++ flask/app.py | 79 +++++++++++++++++++++++++++++++++++++++----- tests/flask_tests.py | 18 ++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index c1d33869..817c21f4 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,11 @@ Relase date to be decided, codename to be chosen. - Malformed JSON data will now trigger a bad request HTTP exception instead of a value error which usually would result in a 500 internal server error if not handled. This is a backwards incompatible change. +- Applications now not only have a root path where the resources and modules + are located but also an instane path which is the designated place to + drop files that are modified at runtime (uploads etc.). Also this is + conceptionally only instance depending and outside version control so it's + the perfect place to put configuration files etc. Version 0.7.3 ------------- diff --git a/flask/app.py b/flask/app.py index 00c36a38..4c969852 100644 --- a/flask/app.py +++ b/flask/app.py @@ -11,6 +11,7 @@ from __future__ import with_statement +import os import sys from threading import Lock from datetime import timedelta @@ -103,14 +104,33 @@ class Flask(_PackageBoundObject): pick up SQL queries in `yourapplication.app` and not `yourapplication.views.frontend`) - .. versionadded:: 0.5 - The `static_path` parameter was added. + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. :param import_name: the name of the application package - :param static_path: can be used to specify a different path for the - static files on the web. Defaults to ``/static``. - This does not affect the folder the files are served - *from*. + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: the folder with static files that should be served + at `static_url_path`. Defaults to the ``'static'`` + folder in the root path of the application. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to `True` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. """ #: The class that is used for request objects. See :class:`~flask.Request` @@ -238,7 +258,8 @@ class Flask(_PackageBoundObject): session_interface = SecureCookieSessionInterface() def __init__(self, import_name, static_path=None, static_url_path=None, - static_folder='static', template_folder='templates'): + static_folder='static', template_folder='templates', + instance_path=None, instance_relative_config=False): _PackageBoundObject.__init__(self, import_name, template_folder=template_folder) if static_path is not None: @@ -251,11 +272,21 @@ class Flask(_PackageBoundObject): self.static_url_path = static_url_path if static_folder is not None: self.static_folder = static_folder + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError('If an instance path is provided it must be ' + 'absolute. A relative path was given instead.') + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods #: to load a config from files. - self.config = Config(self.root_path, self.default_config) + self.config = self.make_config(instance_relative_config) # Prepare the deferred setup of the logger. self._logger = None @@ -491,6 +522,38 @@ class Flask(_PackageBoundObject): """ return self._got_first_request + def make_config(self, instance_relative=False): + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + return Config(root_path, self.default_config) + + def auto_find_instance_path(self): + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + root_mod = sys.modules[self.import_name.split('.')[0]] + instance_path = None + if hasattr(root_mod, '__path__'): + package_dir = os.path.dirname(root_mod.__file__) + instance_path = os.path.join(package_dir, os.path.pardir) + else: + instance_path = os.path.dirname(root_mod.__file__) + basedir = os.path.normpath(os.path.abspath(instance_path)) + return os.path.join(basedir, 'instance') + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` and :meth:`select_jinja_autoescape`. Since 0.7 this also adds diff --git a/tests/flask_tests.py b/tests/flask_tests.py index e5fbf20f..6e9f4555 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1005,6 +1005,24 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = c.post('/foo', data={}, follow_redirects=True) self.assertEqual(rv.data, 'success') + def test_basic_instance_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + app = flask.Flask(__name__) + self.assertEqual(app.instance_path, os.path.join(here, 'instance')) + + app = flask.Flask(__name__, instance_path=here) + self.assertEqual(app.instance_path, here) + + try: + flask.Flask(__name__, instance_path='instance') + except ValueError, e: + self.assert_('must be absolute' in str(e)) + else: + self.fail('Expected value error') + + from blueprintapp import app + self.assertEqual(app.instance_path, os.path.join(here, 'instance')) + class JSONTestCase(unittest.TestCase): From 187cb80dcc1b087f1a7b7d4d67afbd531ad01cd2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 13:55:57 +0200 Subject: [PATCH 0756/3747] Documented instance root --- CHANGES | 3 ++- docs/config.rst | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ flask/app.py | 11 +++++++++ flask/helpers.py | 6 +++-- 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 817c21f4..dc288966 100644 --- a/CHANGES +++ b/CHANGES @@ -30,7 +30,8 @@ Relase date to be decided, codename to be chosen. are located but also an instane path which is the designated place to drop files that are modified at runtime (uploads etc.). Also this is conceptionally only instance depending and outside version control so it's - the perfect place to put configuration files etc. + the perfect place to put configuration files etc. For more information + see :ref:`instance-folders`. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index f50e3180..e1605723 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -267,3 +267,64 @@ your configuration files. However here a list of good recommendations: :ref:`fabric-deployment` pattern. .. _fabric: http://fabfile.org/ + + +.. _instance-folders: + +Instance Folders +---------------- + +.. versionadded:: 0.8 + +Flask 0.8 introduces instance folders. Flask for a long time made it +possible to refer to paths relative to the application's folder directly +(via :attr:`Flask.root_path`). This was also how many developers loaded +configurations stored next to the application. Unfortunately however this +only works well if applications are not packages in which case the root +path refers to the contents of the package. + +With Flask 0.8 a new attribute was introduced: +:attr:`Flask.instance_path`. It refers to a new concept called the +“instance folder”. The instance folder is designed to not be under +version control and be deployment specific. It's the perfect place to +drop things that either change at runtime or configuration files. + +To make it easier to put this folder into an ignore list for your version +control system it's called ``instance`` and placed directly next to your +package or module by default. This path can be overridden by specifying +the `instance_path` parameter to your application:: + + app = Flask(__name__, instance_path='/path/to/instance') + +Please keep in mind that this path *must* be absolute when provided. + +Since the config object provided loading of configuration files from +relative filenames we made it possible to change the loading via filenames +to be relative to the instance path if wanted. The behavior of relative +paths in config files can be flipped between “relative to the application +root” (the default) to “relative to instance folder” via the +`instance_relative_config` switch to the application constructor:: + + app = Flask(__name__, instance_relative_config=True) + +Here is a full example of how to configure Flask to preload the config +from a module and then override the config from a file in the config +folder if it exists:: + + app = Flask(__name__, instance_relative_config=True) + app.config.from_object('yourapplication.default_settings') + app.config.from_pyfile('application.cfg', silent=True) + +The path to the instance folder can be found via the +:attr:`Flask.instance_path`. Flask also provides a shortcut to open a +file from the instnace folder with :meth:`Flask.open_instance_resource`. + +Example usage for both:: + + filename = os.path.join(app.instance_root, 'application.cfg') + with open(filename) as f: + config = f.read() + + # or via open_instance_resource: + with app.open_instance_resource('application.cfg') as f: + config = f.read() diff --git a/flask/app.py b/flask/app.py index 4c969852..636da286 100644 --- a/flask/app.py +++ b/flask/app.py @@ -554,6 +554,17 @@ class Flask(_PackageBoundObject): basedir = os.path.normpath(os.path.abspath(instance_path)) return os.path.join(basedir, 'instance') + def open_instance_resource(self, resource, mode='rb'): + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + return open(os.path.join(self.instance_path, resource), mode) + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` and :meth:`select_jinja_autoescape`. Since 0.7 this also adds diff --git a/flask/helpers.py b/flask/helpers.py index dff79f68..338f5a5a 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -565,7 +565,7 @@ class _PackageBoundObject(object): raise RuntimeError('No static folder for this object') return send_from_directory(self.static_folder, filename) - def open_resource(self, resource): + def open_resource(self, resource, mode='rb'): """Opens a resource from the application's resource folder. To see how this works, consider the following folder structure:: @@ -587,4 +587,6 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. """ - return open(os.path.join(self.root_path, resource), 'rb') + if mode not in ('r', 'rb'): + raise ValueError('Resources can only be opened for reading') + return open(os.path.join(self.root_path, resource), mode) From 9af75546f0b00c92574018498f5979322d10cc4c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 13:57:30 +0200 Subject: [PATCH 0757/3747] Mention default locations for instance folders --- docs/config.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index e1605723..94935a75 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -294,7 +294,18 @@ control system it's called ``instance`` and placed directly next to your package or module by default. This path can be overridden by specifying the `instance_path` parameter to your application:: - app = Flask(__name__, instance_path='/path/to/instance') + app = Flask(__name__, instance_path='/path/to/instance/folder') + +Default locations:: + + Module situation: + /myapp.py + /instance + + Package situation: + /myapp + /__init__.py + /instance Please keep in mind that this path *must* be absolute when provided. From 05204250553b41bb40f788d37d27091b61fc1d59 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 14:00:57 +0200 Subject: [PATCH 0758/3747] Fixed a typo --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 94935a75..57bea926 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -328,7 +328,7 @@ folder if it exists:: The path to the instance folder can be found via the :attr:`Flask.instance_path`. Flask also provides a shortcut to open a -file from the instnace folder with :meth:`Flask.open_instance_resource`. +file from the instance folder with :meth:`Flask.open_instance_resource`. Example usage for both:: From 175d43b2f9c7825b0df4c1049ab571cb1877a807 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 17:46:20 +0200 Subject: [PATCH 0759/3747] Instance paths are now moved into virtualenv/share/appname-instance if installed --- flask/app.py | 41 ++++++++++++++++++++++++++----- flask/helpers.py | 4 ++-- tests/flask_tests.py | 57 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/flask/app.py b/flask/app.py index 636da286..b8f07fcd 100644 --- a/flask/app.py +++ b/flask/app.py @@ -447,6 +447,23 @@ class Flask(_PackageBoundObject): error_handlers = property(_get_error_handlers, _set_error_handlers) del _get_error_handlers, _set_error_handlers + @locked_cached_property + def name(self): + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overriden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == '__main__': + fn = getattr(sys.modules['__main__'], '__file__', None) + if fn is None: + return 'unknown' + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + @property def propagate_exceptions(self): """Returns the value of the `PROPAGATE_EXCEPTIONS` configuration @@ -545,14 +562,26 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.8 """ root_mod = sys.modules[self.import_name.split('.')[0]] - instance_path = None + # we're not using root_mod.__file__ here since the module could be + # virtual. We're trusting the _PackageBoundObject to have calculated + # the proper name. + package_path = self.root_path if hasattr(root_mod, '__path__'): - package_dir = os.path.dirname(root_mod.__file__) - instance_path = os.path.join(package_dir, os.path.pardir) + package_path = os.path.dirname(package_path) + site_parent, site_folder = os.path.split(package_path) + + py_prefix = os.path.abspath(sys.prefix) + if package_path.startswith(py_prefix): + base_dir = py_prefix + elif site_folder == 'site-packages': + parent, folder = os.path.split(site_parent) + if folder.lower() == 'lib': + base_dir = parent + else: + base_dir = site_parent else: - instance_path = os.path.dirname(root_mod.__file__) - basedir = os.path.normpath(os.path.abspath(instance_path)) - return os.path.join(basedir, 'instance') + return os.path.join(package_path, 'instance') + return os.path.join(base_dir, 'share', self.name + '-instance') def open_instance_resource(self, resource, mode='rb'): """Opens a resource from the application's instance folder diff --git a/flask/helpers.py b/flask/helpers.py index 338f5a5a..89fc82cb 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -466,7 +466,7 @@ def send_from_directory(directory, filename, **options): return send_file(filename, conditional=True, **options) -def _get_package_path(name): +def get_package_path(name): """Returns the path to a package or cwd if that cannot be found.""" try: return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) @@ -512,7 +512,7 @@ class _PackageBoundObject(object): self.template_folder = template_folder #: Where is the app root located? - self.root_path = _get_package_path(self.import_name) + self.root_path = get_package_path(self.import_name) self._static_folder = None self._static_url_path = None diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 6e9f4555..63ecad97 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1005,7 +1005,10 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = c.post('/foo', data={}, follow_redirects=True) self.assertEqual(rv.data, 'success') - def test_basic_instance_paths(self): + +class InstanceTestCase(unittest.TestCase): + + def test_uninstalled_module_paths(self): here = os.path.abspath(os.path.dirname(__file__)) app = flask.Flask(__name__) self.assertEqual(app.instance_path, os.path.join(here, 'instance')) @@ -1020,9 +1023,60 @@ class BasicFunctionalityTestCase(unittest.TestCase): else: self.fail('Expected value error') + def test_uninstalled_package_paths(self): from blueprintapp import app + here = os.path.abspath(os.path.dirname(__file__)) self.assertEqual(app.instance_path, os.path.join(here, 'instance')) + def test_installed_module_paths(self): + import types + expected_prefix = os.path.abspath('foo') + mod = types.ModuleType('myapp') + mod.__file__ = os.path.join(expected_prefix, 'lib', + 'site-packages', 'myapp.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assertEqual(mod.app.instance_path, + os.path.join(expected_prefix, 'share', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_installed_package_paths(self): + import types + expected_prefix = os.path.abspath('foo') + package_path = os.path.join(expected_prefix, 'lib', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assertEqual(mod.app.instance_path, + os.path.join(expected_prefix, 'share', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_prefix_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assertEqual(mod.app.instance_path, + os.path.join(expected_prefix, 'share', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + class JSONTestCase(unittest.TestCase): @@ -2114,6 +2168,7 @@ def suite(): suite.addTest(unittest.makeSuite(SubdomainTestCase)) suite.addTest(unittest.makeSuite(ViewTestCase)) suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + suite.addTest(unittest.makeSuite(InstanceTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) if flask.signals_available: From e328eba97cc87a34e846693b62d17bdedaf27134 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 17:51:24 +0200 Subject: [PATCH 0760/3747] Corrected prefix detection --- flask/app.py | 2 ++ tests/flask_tests.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index b8f07fcd..03c7ce32 100644 --- a/flask/app.py +++ b/flask/app.py @@ -577,6 +577,8 @@ class Flask(_PackageBoundObject): parent, folder = os.path.split(site_parent) if folder.lower() == 'lib': base_dir = parent + elif os.path.basename(parent).lower() == 'lib': + base_dir = os.path.dirname(parent) else: base_dir = site_parent else: diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 63ecad97..7b3f334e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1032,7 +1032,7 @@ class InstanceTestCase(unittest.TestCase): import types expected_prefix = os.path.abspath('foo') mod = types.ModuleType('myapp') - mod.__file__ = os.path.join(expected_prefix, 'lib', + mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages', 'myapp.py') sys.modules['myapp'] = mod try: @@ -1046,7 +1046,7 @@ class InstanceTestCase(unittest.TestCase): def test_installed_package_paths(self): import types expected_prefix = os.path.abspath('foo') - package_path = os.path.join(expected_prefix, 'lib', + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages', 'myapp') mod = types.ModuleType('myapp') mod.__path__ = [package_path] @@ -1063,7 +1063,7 @@ class InstanceTestCase(unittest.TestCase): def test_prefix_installed_paths(self): import types expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages', 'myapp') mod = types.ModuleType('myapp') mod.__path__ = [package_path] From 74f4af59f631ef154ffe6f34d8e525a05f97d4a6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 17:51:49 +0200 Subject: [PATCH 0761/3747] Added comments for two branches in the instance detection --- flask/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/app.py b/flask/app.py index 03c7ce32..2cad86e2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -575,8 +575,10 @@ class Flask(_PackageBoundObject): base_dir = py_prefix elif site_folder == 'site-packages': parent, folder = os.path.split(site_parent) + # Windows like installations if folder.lower() == 'lib': base_dir = parent + # UNIX like installations elif os.path.basename(parent).lower() == 'lib': base_dir = os.path.dirname(parent) else: From fb1a6730cfbf63b6d6ccda93381f0823d8b6c85a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 18:00:16 +0200 Subject: [PATCH 0762/3747] Leave eggs when finding the instance path --- flask/app.py | 6 +++++- tests/flask_tests.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 2cad86e2..14b6d138 100644 --- a/flask/app.py +++ b/flask/app.py @@ -568,8 +568,12 @@ class Flask(_PackageBoundObject): package_path = self.root_path if hasattr(root_mod, '__path__'): package_path = os.path.dirname(package_path) - site_parent, site_folder = os.path.split(package_path) + # leave the egg wrapper folder or the actual .egg on the filesystem + if os.path.basename(package_path).endswith('.egg'): + package_path = os.path.dirname(package_path) + + site_parent, site_folder = os.path.split(package_path) py_prefix = os.path.abspath(sys.prefix) if package_path.startswith(py_prefix): base_dir = py_prefix diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7b3f334e..4b0150af 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1077,6 +1077,23 @@ class InstanceTestCase(unittest.TestCase): finally: sys.modules['myapp'] = None + def test_egg_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'MyApp.egg', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assertEqual(mod.app.instance_path, + os.path.join(expected_prefix, 'share', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + class JSONTestCase(unittest.TestCase): From b3aaf6d5ca4fa53cc78456b2df01ab4d11cec84a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 23:19:33 +0200 Subject: [PATCH 0763/3747] Refactored package finding --- flask/app.py | 34 +++++----------------------------- flask/helpers.py | 37 +++++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 8 ++++---- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/flask/app.py b/flask/app.py index 14b6d138..52a54b29 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,7 +24,8 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed, BadRequest from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - locked_cached_property, _tojson_filter, _endpoint_from_view_func + locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ + find_package from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import RequestContext @@ -561,35 +562,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.8 """ - root_mod = sys.modules[self.import_name.split('.')[0]] - # we're not using root_mod.__file__ here since the module could be - # virtual. We're trusting the _PackageBoundObject to have calculated - # the proper name. - package_path = self.root_path - if hasattr(root_mod, '__path__'): - package_path = os.path.dirname(package_path) - - # leave the egg wrapper folder or the actual .egg on the filesystem - if os.path.basename(package_path).endswith('.egg'): - package_path = os.path.dirname(package_path) - - site_parent, site_folder = os.path.split(package_path) - py_prefix = os.path.abspath(sys.prefix) - if package_path.startswith(py_prefix): - base_dir = py_prefix - elif site_folder == 'site-packages': - parent, folder = os.path.split(site_parent) - # Windows like installations - if folder.lower() == 'lib': - base_dir = parent - # UNIX like installations - elif os.path.basename(parent).lower() == 'lib': - base_dir = os.path.dirname(parent) - else: - base_dir = site_parent - else: + prefix, package_path = find_package(self.import_name) + if prefix is None: return os.path.join(package_path, 'instance') - return os.path.join(base_dir, 'share', self.name + '-instance') + return os.path.join(prefix, 'var', self.name + '-instance') def open_instance_resource(self, resource, mode='rb'): """Opens a resource from the application's instance folder diff --git a/flask/helpers.py b/flask/helpers.py index 89fc82cb..d3ce50b8 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -474,6 +474,43 @@ def get_package_path(name): return os.getcwd() +def find_package(import_name): + """Finds a package and returns the prefix (or None if the package is + not installed) as well as the folder that contains the package or + module as a tuple. + """ + root_mod = sys.modules[import_name.split('.')[0]] + package_path = getattr(root_mod, '__file__', None) + if package_path is None: + package_path = os.getcwd() + else: + package_path = os.path.abspath(os.path.dirname(package_path)) + if hasattr(root_mod, '__path__'): + package_path = os.path.dirname(package_path) + + # leave the egg wrapper folder or the actual .egg on the filesystem + test_package_path = package_path + if os.path.basename(test_package_path).endswith('.egg'): + test_package_path = os.path.dirname(test_package_path) + + site_parent, site_folder = os.path.split(test_package_path) + py_prefix = os.path.abspath(sys.prefix) + if test_package_path.startswith(py_prefix): + return py_prefix, package_path + elif site_folder.lower() == 'site-packages': + parent, folder = os.path.split(site_parent) + # Windows like installations + if folder.lower() == 'lib': + base_dir = parent + # UNIX like installations + elif os.path.basename(parent).lower() == 'lib': + base_dir = os.path.dirname(parent) + else: + base_dir = site_parent + return base_dir, package_path + return None, package_path + + class locked_cached_property(object): """A decorator that converts a function into a lazy property. The function wrapped is called the first time to retrieve the result diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 4b0150af..4404f931 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1038,7 +1038,7 @@ class InstanceTestCase(unittest.TestCase): try: mod.app = flask.Flask(mod.__name__) self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'share', + os.path.join(expected_prefix, 'var', 'myapp-instance')) finally: sys.modules['myapp'] = None @@ -1055,7 +1055,7 @@ class InstanceTestCase(unittest.TestCase): try: mod.app = flask.Flask(mod.__name__) self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'share', + os.path.join(expected_prefix, 'var', 'myapp-instance')) finally: sys.modules['myapp'] = None @@ -1072,7 +1072,7 @@ class InstanceTestCase(unittest.TestCase): try: mod.app = flask.Flask(mod.__name__) self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'share', + os.path.join(expected_prefix, 'var', 'myapp-instance')) finally: sys.modules['myapp'] = None @@ -1089,7 +1089,7 @@ class InstanceTestCase(unittest.TestCase): try: mod.app = flask.Flask(mod.__name__) self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'share', + os.path.join(expected_prefix, 'var', 'myapp-instance')) finally: sys.modules['myapp'] = None From c3844d11026f84400d5a088dd5c24c6a4a04dcd1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 23:21:43 +0200 Subject: [PATCH 0764/3747] Rename _get_package_path to get_root_path to avoid confusion --- flask/helpers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index d3ce50b8..04d23a07 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -466,8 +466,12 @@ def send_from_directory(directory, filename, **options): return send_file(filename, conditional=True, **options) -def get_package_path(name): - """Returns the path to a package or cwd if that cannot be found.""" +def get_root_path(name): + """Returns the path to a package or cwd if that cannot be found. This + returns the path of a package or the folder that contains a module. + + Not to be confused with the package path returned by :func:`find_package`. + """ try: return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) except (KeyError, AttributeError): @@ -549,7 +553,7 @@ class _PackageBoundObject(object): self.template_folder = template_folder #: Where is the app root located? - self.root_path = get_package_path(self.import_name) + self.root_path = get_root_path(self.import_name) self._static_folder = None self._static_url_path = None From 1d6f86bc87253c1dc1cf0c156f421214c38c3202 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 23:25:33 +0200 Subject: [PATCH 0765/3747] Updated docstring --- flask/helpers.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 04d23a07..a260b03f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -466,23 +466,29 @@ def send_from_directory(directory, filename, **options): return send_file(filename, conditional=True, **options) -def get_root_path(name): +def get_root_path(import_name): """Returns the path to a package or cwd if that cannot be found. This returns the path of a package or the folder that contains a module. Not to be confused with the package path returned by :func:`find_package`. """ + __import__(import_name) try: - return os.path.abspath(os.path.dirname(sys.modules[name].__file__)) - except (KeyError, AttributeError): + directory = os.path.dirname(sys.modules[import_name].__file__) + return os.path.abspath(directory) + except AttributeError: return os.getcwd() def find_package(import_name): """Finds a package and returns the prefix (or None if the package is not installed) as well as the folder that contains the package or - module as a tuple. + module as a tuple. The package path returned is the module that would + have to be added to the pythonpath in order to make it possible to + import the module. The prefix is the path below which a UNIX like + folder structure exists (lib, share etc.). """ + __import__(import_name) root_mod = sys.modules[import_name.split('.')[0]] package_path = getattr(root_mod, '__file__', None) if package_path is None: From eb9a14e1581fd379f25986bb206fe0d8fdc6b1a3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 23:40:53 +0200 Subject: [PATCH 0766/3747] Split up a test into two --- tests/flask_tests.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 4404f931..220bf4cb 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1008,14 +1008,8 @@ class BasicFunctionalityTestCase(unittest.TestCase): class InstanceTestCase(unittest.TestCase): - def test_uninstalled_module_paths(self): + def test_explicit_instance_paths(self): here = os.path.abspath(os.path.dirname(__file__)) - app = flask.Flask(__name__) - self.assertEqual(app.instance_path, os.path.join(here, 'instance')) - - app = flask.Flask(__name__, instance_path=here) - self.assertEqual(app.instance_path, here) - try: flask.Flask(__name__, instance_path='instance') except ValueError, e: @@ -1023,6 +1017,14 @@ class InstanceTestCase(unittest.TestCase): else: self.fail('Expected value error') + app = flask.Flask(__name__, instance_path=here) + self.assertEqual(app.instance_path, here) + + def test_uninstalled_module_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + app = flask.Flask(__name__) + self.assertEqual(app.instance_path, os.path.join(here, 'instance')) + def test_uninstalled_package_paths(self): from blueprintapp import app here = os.path.abspath(os.path.dirname(__file__)) From 45d963d6daa4315388530d58c777b838d5465c65 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 10 Aug 2011 23:50:43 +0200 Subject: [PATCH 0767/3747] The session interface is new in 0.8 not 0.7 --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 52a54b29..35577984 100644 --- a/flask/app.py +++ b/flask/app.py @@ -255,7 +255,7 @@ class Flask(_PackageBoundObject): #: the session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. #: - #: .. versionadded:: 0.7 + #: .. versionadded:: 0.8 session_interface = SecureCookieSessionInterface() def __init__(self, import_name, static_path=None, static_url_path=None, From 77492f8afad5b8a3cc8bca297bf40c73b9e18ff4 Mon Sep 17 00:00:00 2001 From: Alexis Metaireau Date: Sat, 13 Aug 2011 01:22:10 +0300 Subject: [PATCH 0768/3747] typo --- docs/signals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index a5821603..0d1d9eea 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -124,7 +124,7 @@ debugging. You can access the name of the signal with the .. admonition:: For Extension Developers - If you are writing a Flask extension and you to gracefully degrade for + If you are writing a Flask extension and you want to gracefully degrade for missing blinker installations, you can do so by using the :class:`flask.signals.Namespace` class. From 713ced603e246b34fd88a417ae0be4d71505be70 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Wed, 17 Aug 2011 08:38:34 +0300 Subject: [PATCH 0769/3747] Improve configuration docs a bit --- docs/config.rst | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 57bea926..4f37b9b9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -5,9 +5,10 @@ Configuration Handling .. versionadded:: 0.3 -Applications need some kind of configuration. There are different things -you might want to change like toggling debug mode, the secret key, and a -lot of very similar things. +Applications need some kind of configuration. There are different settings +you might want to change depending on the application environment like +toggling the debug mode, setting the secret key, and other such +environment-specific things. The way Flask is designed usually requires the configuration to be available when the application starts up. You can hardcode the @@ -31,8 +32,7 @@ can be modified just like any dictionary:: app.config['DEBUG'] = True Certain configuration values are also forwarded to the -:attr:`~flask.Flask` object so that you can read and write them from -there:: +:attr:`~flask.Flask` object so you can read and write them from there:: app.debug = True @@ -139,10 +139,11 @@ The following configuration values are used internally by Flask: Configuring from Files ---------------------- -Configuration becomes more useful if you can configure from a file, and -ideally that file would be outside of the actual application package so that -you can install the package with distribute (:ref:`distribute-deployment`) -and still modify that file afterwards. +Configuration becomes more useful if you can store it in a separate file, +ideally located outside the actual application package. This makes +packaging and distributing your application possible via various package +handling tools (:ref:`distribute-deployment`) and finally modifying the +configuration file afterwards. So a common pattern is this:: @@ -170,12 +171,13 @@ The configuration files themselves are actual Python files. Only values in uppercase are actually stored in the config object later on. So make sure to use uppercase letters for your config keys. -Here is an example configuration file:: +Here is an example of a configuration file:: + # Example configuration DEBUG = False SECRET_KEY = '?\xbf,\xb4\x8d\xa3"<\x9c\xb0@\x0f5\xab,w\xee\x8d$0\x13\x8b83' -Make sure to load the configuration very early on so that extensions have +Make sure to load the configuration very early on, so that extensions have the ability to access the configuration when starting up. There are other methods on the config object as well to load from individual files. For a complete reference, read the :class:`~flask.Config` object's @@ -186,9 +188,9 @@ Configuration Best Practices ---------------------------- The downside with the approach mentioned earlier is that it makes testing -a little harder. There is no one 100% solution for this problem in -general, but there are a couple of things you can do to improve that -experience: +a little harder. There is no single 100% solution for this problem in +general, but there are a couple of things you can keep in mind to improve +that experience: 1. create your application in a function and register blueprints on it. That way you can create multiple instances of your application with @@ -203,10 +205,10 @@ experience: Development / Production ------------------------ -Most applications need more than one configuration. There will at least -be a separate configuration for a production server and one used during -development. The easiest way to handle this is to use a default -configuration that is always loaded and part of version control, and a +Most applications need more than one configuration. There should be at +least separate configurations for the production server and the one used +during development. The easiest way to handle this is to use a default +configuration that is always loaded and part of the version control, and a separate configuration that overrides the values as necessary as mentioned in the example above:: From 68ec5a30687a89a56406d8abd603b51a6645856e Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 25 Aug 2011 11:46:05 +0100 Subject: [PATCH 0770/3747] Allow passing the endpoint to the route decorators on Flask's `application` and `blueprints`. --- flask/app.py | 3 ++- flask/blueprints.py | 3 ++- tests/flask_tests.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 35577984..0597ba5b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -942,7 +942,8 @@ class Flask(_PackageBoundObject): :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - self.add_url_rule(rule, None, f, **options) + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator diff --git a/flask/blueprints.py b/flask/blueprints.py index 075961ab..6d693781 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -157,7 +157,8 @@ class Blueprint(_PackageBoundObject): :func:`url_for` function is prefixed with the name of the blueprint. """ def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) + endpoint = options.pop("endpoint", f.__name__) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 220bf4cb..3753218e 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1005,6 +1005,32 @@ class BasicFunctionalityTestCase(unittest.TestCase): rv = c.post('/foo', data={}, follow_redirects=True) self.assertEqual(rv.data, 'success') + def test_route_decorator_custom_endpoint(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/foo/') + def foo(): + return flask.request.endpoint + + @app.route('/bar/', endpoint='bar') + def for_bar(): + return flask.request.endpoint + + @app.route('/bar/123', endpoint='123') + def for_bar_foo(): + return flask.request.endpoint + + with app.test_request_context(): + assert flask.url_for('foo') == '/foo/' + assert flask.url_for('bar') == '/bar/' + assert flask.url_for('123') == '/bar/123' + + c = app.test_client() + self.assertEqual(c.get('/foo/').data, 'foo') + self.assertEqual(c.get('/bar/').data, 'bar') + self.assertEqual(c.get('/bar/123').data, '123') + class InstanceTestCase(unittest.TestCase): @@ -1687,6 +1713,40 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/').data, '1') self.assertEqual(c.get('/page/2').data, '2') + def test_route_decorator_custom_endpoint(self): + + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + @bp.route('/bar', endpoint='bar') + def foo_bar(): + return flask.request.endpoint + + @bp.route('/bar/123', endpoint='123') + def foo_bar_foo(): + return flask.request.endpoint + + @bp.route('/bar/foo') + def bar_foo(): + return flask.request.endpoint + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + @app.route('/') + def index(): + return flask.request.endpoint + + c = app.test_client() + self.assertEqual(c.get('/').data, 'index') + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/bar').data, 'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, 'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + class SendfileTestCase(unittest.TestCase): From 04e87a93def1593dd886931e0f9e1d4f37f8d243 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 25 Aug 2011 12:01:41 +0100 Subject: [PATCH 0771/3747] Add endpoint parameter documentation to the route decorator. --- flask/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask/app.py b/flask/app.py index 0597ba5b..67314fc8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -938,6 +938,9 @@ class Flask(_PackageBoundObject): subdomain matching is in use. :param strict_slashes: can be used to disable the strict slashes setting for this rule. See above. + :param endpoint: Since version 0.8 you can also pass the enpoint, + it will be used instead of generating the endpoint + from the function name. :param options: other options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. """ From c844d02f1c7558e959891de75494cffd22dc323f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 12:13:55 +0100 Subject: [PATCH 0772/3747] Added the APPLICATION_ROOT configuration variable which is used by session backends. --- CHANGES | 1 + docs/config.rst | 10 +++++++++- flask/app.py | 1 + flask/sessions.py | 12 ++++++++++-- tests/flask_tests.py | 22 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index dc288966..6ba980aa 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,7 @@ Relase date to be decided, codename to be chosen. conceptionally only instance depending and outside version control so it's the perfect place to put configuration files etc. For more information see :ref:`instance-folders`. +- Added the ``APPLICATION_ROOT`` configuration variable. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index 57bea926..eb9d0e06 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -77,6 +77,13 @@ The following configuration values are used internally by Flask: ``SERVER_NAME`` the name and port number of the server. Required for subdomain support (e.g.: ``'localhost:5000'``) +``APPLICATION_ROOT`` If the application does not occupy + a whole domain or subdomain this can + be set to the path where the application + is configured to live. This is for + session cookie as path value. If + domains are used, this should be + ``None``. ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by @@ -134,7 +141,8 @@ The following configuration values are used internally by Flask: ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION`` .. versionadded:: 0.8 - ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS`` + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, + ``APPLICATION_ROOT`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 35577984..6ad69975 100644 --- a/flask/app.py +++ b/flask/app.py @@ -236,6 +236,7 @@ class Flask(_PackageBoundObject): 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, + 'APPLICATION_ROOT': None, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False diff --git a/flask/sessions.py b/flask/sessions.py index ee006cda..fda84a25 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -127,6 +127,13 @@ class SessionInterface(object): # chop of the port which is usually not supported by browsers return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + def get_cookie_path(self, app): + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the ``APPLICATION_ROOT`` + configuration variable or uses ``/`` if it's `None`. + """ + return app.config['APPLICATION_ROOT'] or '/' + def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session or `None` if the session is linked to the browser session. The @@ -169,9 +176,10 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) if session.modified and not session: - response.delete_cookie(app.session_cookie_name, + response.delete_cookie(app.session_cookie_name, path=path, domain=domain) else: - session.save_cookie(response, app.session_cookie_name, + session.save_cookie(response, app.session_cookie_name, path=path, expires=expires, httponly=True, domain=domain) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 220bf4cb..db8275d6 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -339,6 +339,28 @@ class BasicFunctionalityTestCase(unittest.TestCase): assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() + def test_session_using_application_root(self): + class PrefixPathMiddleware(object): + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + app = flask.Flask(__name__) + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') + app.config.update( + SECRET_KEY='foo', + APPLICATION_ROOT='/bar' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + assert 'path=/bar' in rv.headers['set-cookie'].lower() + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): From a5da2c98f3ed2f9fe254e3b61799b06fa230a175 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 15:18:39 +0100 Subject: [PATCH 0773/3747] Implemented flask.testing.TestClient.session_transaction for quick session modifications in test environments. --- CHANGES | 2 ++ docs/api.rst | 9 +++++++ flask/app.py | 2 ++ flask/testing.py | 58 +++++++++++++++++++++++++++++++++++++++++--- tests/flask_tests.py | 45 ++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 6ba980aa..a451b978 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,8 @@ Relase date to be decided, codename to be chosen. the perfect place to put configuration files etc. For more information see :ref:`instance-folders`. - Added the ``APPLICATION_ROOT`` configuration variable. +- Implemented :meth:`~flask.testing.TestClient.session_transaction` to + easily modify sessions from the test environment. Version 0.7.3 ------------- diff --git a/docs/api.rst b/docs/api.rst index 6b695bfa..f4fab86f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -218,6 +218,15 @@ implementation that Flask is using. :members: +Test Client +----------- + +.. currentmodule:: flask.testing + +.. autoclass:: TestClient + :members: + + Application Globals ------------------- diff --git a/flask/app.py b/flask/app.py index 6ad69975..20cbca52 100644 --- a/flask/app.py +++ b/flask/app.py @@ -706,6 +706,8 @@ class Flask(_PackageBoundObject): rv = c.get('/?vodka=42') assert request.args['vodka'] == '42' + See :class:`~flask.testing.TestClient` for more information. + .. versionchanged:: 0.4 added support for `with` block usage for the client. diff --git a/flask/testing.py b/flask/testing.py index 06a2c016..c1844c00 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,19 +10,69 @@ :license: BSD, see LICENSE for more details. """ +from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack class FlaskClient(Client): - """Works like a regular Werkzeug test client but has some - knowledge about how Flask works to defer the cleanup of the - request context stack to the end of a with body when used - in a with statement. + """Works like a regular Werkzeug test client but has some knowledge about + how Flask works to defer the cleanup of the request context stack to the + end of a with body when used in a with statement. For general information + about how to use this class refer to :class:`werkzeug.test.Client`. + + Basic usage is outlined in the :ref:`testing` chapter. """ preserve_context = context_preserved = False + @contextmanager + def session_transaction(self, *args, **kwargs): + """When used in combination with a with statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the with block is left the session is + stored back. + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + app = self.application + environ_overrides = kwargs.pop('environ_overrides', {}) + if self.cookie_jar is not None: + self.cookie_jar.inject_wsgi(environ_overrides) + outer_reqctx = _request_ctx_stack.top + with app.test_request_context(*args, **kwargs) as c: + sess = app.open_session(c.request) + if sess is None: + raise RuntimeError('Session backend did not open a session. ' + 'Check the configuration') + + # Since we have to open a new request context for the session + # handling we want to make sure that we hide out own context + # from the caller. By pushing the original request context + # (or None) on top of this and popping it we get exactly that + # behavior. It's important to not use the push and pop + # methods of the actual request context object since that would + # mean that cleanup handlers are called + _request_ctx_stack.push(outer_reqctx) + try: + yield sess + finally: + _request_ctx_stack.pop() + + resp = app.response_class() + if not app.session_interface.is_null_session(sess): + app.save_session(sess, resp) + if self.cookie_jar is not None: + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) + def open(self, *args, **kwargs): if self.context_preserved: _request_ctx_stack.pop() diff --git a/tests/flask_tests.py b/tests/flask_tests.py index db8275d6..3c125be4 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1028,6 +1028,50 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(rv.data, 'success') +class TestToolsTestCase(unittest.TestCase): + + def test_session_transactions(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + @app.route('/') + def index(): + return unicode(flask.session['foo']) + + with app.test_client() as c: + with c.session_transaction() as sess: + self.assertEqual(len(sess), 0) + sess['foo'] = [42] + self.assertEqual(len(sess), 1) + rv = c.get('/') + self.assertEqual(rv.data, '[42]') + + def test_session_transactions_no_null_sessions(self): + app = flask.Flask(__name__) + app.testing = True + + with app.test_client() as c: + try: + with c.session_transaction() as sess: + pass + except RuntimeError, e: + self.assert_('Session backend did not open a session' in str(e)) + else: + self.fail('Expected runtime error') + + def test_session_transactions_keep_context(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + with app.test_client() as c: + rv = c.get('/') + req = flask.request._get_current_object() + with c.session_transaction(): + self.assert_(req is flask.request._get_current_object()) + + class InstanceTestCase(unittest.TestCase): def test_explicit_instance_paths(self): @@ -2209,6 +2253,7 @@ def suite(): suite.addTest(unittest.makeSuite(SubdomainTestCase)) suite.addTest(unittest.makeSuite(ViewTestCase)) suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + suite.addTest(unittest.makeSuite(TestToolsTestCase)) suite.addTest(unittest.makeSuite(InstanceTestCase)) if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) From 001a5128d87e2cb934d14244d901d47e01980873 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 15:20:40 +0100 Subject: [PATCH 0774/3747] Refactored tests to use a different subclass --- tests/flask_tests.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3c125be4..743339f0 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -95,7 +95,11 @@ def emits_module_deprecation_warning(f): return update_wrapper(new_f, f) -class ContextTestCase(unittest.TestCase): +class FlaskTestCase(unittest.TestCase): + pass + + +class ContextTestCase(FlaskTestCase): def test_context_binding(self): app = flask.Flask(__name__) @@ -172,7 +176,7 @@ class ContextTestCase(unittest.TestCase): raise AssertionError('some kind of exception expected') -class BasicFunctionalityTestCase(unittest.TestCase): +class BasicFunctionalityTestCase(FlaskTestCase): def test_options_work(self): app = flask.Flask(__name__) @@ -1028,7 +1032,7 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(rv.data, 'success') -class TestToolsTestCase(unittest.TestCase): +class TestToolsTestCase(FlaskTestCase): def test_session_transactions(self): app = flask.Flask(__name__) @@ -1072,7 +1076,7 @@ class TestToolsTestCase(unittest.TestCase): self.assert_(req is flask.request._get_current_object()) -class InstanceTestCase(unittest.TestCase): +class InstanceTestCase(FlaskTestCase): def test_explicit_instance_paths(self): here = os.path.abspath(os.path.dirname(__file__)) @@ -1163,7 +1167,7 @@ class InstanceTestCase(unittest.TestCase): sys.modules['myapp'] = None -class JSONTestCase(unittest.TestCase): +class JSONTestCase(FlaskTestCase): def test_json_bad_requests(self): app = flask.Flask(__name__) @@ -1239,7 +1243,7 @@ class JSONTestCase(unittest.TestCase): test_modified_url_encoding = None -class TemplatingTestCase(unittest.TestCase): +class TemplatingTestCase(FlaskTestCase): def test_context_processing(self): app = flask.Flask(__name__) @@ -1361,7 +1365,7 @@ class TemplatingTestCase(unittest.TestCase): assert rv.data == 'Hello Custom World!' -class ModuleTestCase(unittest.TestCase): +class ModuleTestCase(FlaskTestCase): @emits_module_deprecation_warning def test_basic_module(self): @@ -1587,7 +1591,7 @@ class ModuleTestCase(unittest.TestCase): assert c.get('/foo/bar').data == 'bar' -class BlueprintTestCase(unittest.TestCase): +class BlueprintTestCase(FlaskTestCase): def test_blueprint_specific_error_handling(self): frontend = flask.Blueprint('frontend', __name__) @@ -1754,7 +1758,7 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/page/2').data, '2') -class SendfileTestCase(unittest.TestCase): +class SendfileTestCase(FlaskTestCase): def test_send_file_regular(self): app = flask.Flask(__name__) @@ -1854,7 +1858,7 @@ class SendfileTestCase(unittest.TestCase): assert options['filename'] == 'index.txt' -class LoggingTestCase(unittest.TestCase): +class LoggingTestCase(FlaskTestCase): def test_logger_cache(self): app = flask.Flask(__name__) @@ -1938,7 +1942,7 @@ class LoggingTestCase(unittest.TestCase): assert rv.data == 'Hello Server Error' -class ConfigTestCase(unittest.TestCase): +class ConfigTestCase(FlaskTestCase): def common_object_test(self, app): assert app.secret_key == 'devkey' @@ -1998,7 +2002,7 @@ class ConfigTestCase(unittest.TestCase): assert not app.config.from_pyfile('missing.cfg', silent=True) -class SubdomainTestCase(unittest.TestCase): +class SubdomainTestCase(FlaskTestCase): def test_basic_support(self): app = flask.Flask(__name__) @@ -2061,7 +2065,7 @@ class SubdomainTestCase(unittest.TestCase): assert rv.data == 'Outside' -class TestSignals(unittest.TestCase): +class TestSignals(FlaskTestCase): def test_template_rendered(self): app = flask.Flask(__name__) @@ -2144,7 +2148,7 @@ class TestSignals(unittest.TestCase): flask.got_request_exception.disconnect(record, app) -class ViewTestCase(unittest.TestCase): +class ViewTestCase(FlaskTestCase): def common_test(self, app): c = app.test_client() @@ -2219,7 +2223,7 @@ class ViewTestCase(unittest.TestCase): self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) -class DeprecationsTestCase(unittest.TestCase): +class DeprecationsTestCase(FlaskTestCase): def test_init_jinja_globals(self): class MyFlask(flask.Flask): From f051939d8ba1c5ec585fd4db1f83237a66020c0a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 15:24:10 +0100 Subject: [PATCH 0775/3747] Test that we're not leaking a request context in the testsuite, fixed a leak --- tests/flask_tests.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 743339f0..62814882 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -96,7 +96,15 @@ def emits_module_deprecation_warning(f): class FlaskTestCase(unittest.TestCase): - pass + + def ensure_clean_request_context(self): + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + self.assertEqual(flask._request_ctx_stack.top, None) + + def tearDown(self): + unittest.TestCase.tearDown(self) + self.ensure_clean_request_context() class ContextTestCase(FlaskTestCase): @@ -1881,23 +1889,23 @@ class LoggingTestCase(FlaskTestCase): @app.route('/exc') def exc(): 1/0 - c = app.test_client() - with catch_stderr() as err: - c.get('/') - out = err.getvalue() - assert 'WARNING in flask_tests [' in out - assert 'flask_tests.py' in out - assert 'the standard library is dead' in out - assert 'this is a debug statement' in out + with app.test_client() as c: + with catch_stderr() as err: + c.get('/') + out = err.getvalue() + assert 'WARNING in flask_tests [' in out + assert 'flask_tests.py' in out + assert 'the standard library is dead' in out + assert 'this is a debug statement' in out - with catch_stderr() as err: - try: - c.get('/exc') - except ZeroDivisionError: - pass - else: - assert False, 'debug log ate the exception' + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + assert False, 'debug log ate the exception' def test_exception_logging(self): out = StringIO() From 311ac0f533bf5a308200a51b861a9728b9223276 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 15:33:03 +0100 Subject: [PATCH 0776/3747] Ensure that nobody can nest test client invocations --- flask/testing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/testing.py b/flask/testing.py index c1844c00..dfdaff57 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -108,6 +108,8 @@ class FlaskClient(Client): self.context_preserved = _request_ctx_stack.top is not old def __enter__(self): + if self.preserve_context: + raise RuntimeError('Cannot nest client invocations') self.preserve_context = True return self From 1ea3d4ea5300d9f456622da0cc55394250fa9b4c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 16:13:43 +0100 Subject: [PATCH 0777/3747] Updated documentation regarding the session transactions --- docs/testing.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/testing.rst b/docs/testing.rst index ed5765ea..ed44e5a2 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -273,3 +273,35 @@ is no longer available (because you are trying to use it outside of the actual r However, keep in mind that any :meth:`~flask.Flask.after_request` functions are already called at this point so your database connection and everything involved is probably already closed down. + + +Accessing and Modifying Sessions +-------------------------------- + +.. versionadded:: 0.8 + +Sometimes it can be very helpful to access or modify the sessions from the +test client. Generally there are two ways for this. Ify ou just want to +ensure that a session has certain keys set to certain values you can just +keep the context around and access :data:`flask.session`:: + + with app.test_client() as c: + rv = c.get('/') + assert flask.session['foo'] == 42 + +This however does not make it possible to also modify the session or to +access the session before a request was fired. Starting with Flask 0.8 we +provide a so called “session transaction” which simulates the appropriate +calls to open a session in the context of the test client and to modify +it. At the end of the transaction the session is stored. This works +independently of the session backend used:: + + with app.test_client() as c: + with c.session_transaction() as sess: + sess['a_key'] = 'a value' + + # once this is reached the session was stored + +Note that in this case you have to use the ``sess`` object instead of the +:data:`flask.session` proxy. The object however itself will provide the +same interface. From 03a71e022919a0e1bff6606400e9d5ae234b8244 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 25 Aug 2011 17:26:57 +0100 Subject: [PATCH 0778/3747] Deny dot's in blueprint endpoints. Add tests for that too. --- flask/blueprints.py | 2 ++ tests/flask_tests.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/flask/blueprints.py b/flask/blueprints.py index 6d693781..8bfc610e 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -166,6 +166,8 @@ class Blueprint(_PackageBoundObject): """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for the :func:`url_for` function is prefixed with the name of the blueprint. """ + if endpoint: + assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 3753218e..d5f7413d 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1747,6 +1747,57 @@ class BlueprintTestCase(unittest.TestCase): self.assertEqual(c.get('/py/bar/123').data, 'bp.123') self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + def test_route_decorator_custom_endpoint_with_dots(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/foo') + def foo(): + return flask.request.endpoint + + try: + @bp.route('/bar', endpoint='bar.bar') + def foo_bar(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + try: + @bp.route('/bar/123', endpoint='bar.123') + def foo_bar_foo(): + return flask.request.endpoint + except AssertionError: + pass + else: + raise AssertionError('expected AssertionError not raised') + + def foo_foo_foo(): + pass + + self.assertRaises( + AssertionError, + lambda: bp.add_url_rule( + '/bar/123', endpoint='bar.123', view_func=foo_foo_foo + ) + ) + + self.assertRaises( + AssertionError, + bp.route('/bar/123', endpoint='bar.123'), + lambda: None + ) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + + c = app.test_client() + self.assertEqual(c.get('/py/foo').data, 'bp.foo') + # The rule's din't actually made it through + rv = c.get('/py/bar') + assert rv.status_code == 404 + rv = c.get('/py/bar/123') + assert rv.status_code == 404 class SendfileTestCase(unittest.TestCase): From e853a0f73959ad12cdabc2d74324cdc9329ae794 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 20:47:50 +0100 Subject: [PATCH 0779/3747] The test client and test_request_context are now both using the same logic internally for creating the environ. Also they use APPLICATION_ROOT now. --- CHANGES | 3 +++ docs/api.rst | 2 +- flask/app.py | 21 +++++++-------------- flask/testing.py | 26 ++++++++++++-------------- tests/flask_tests.py | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/CHANGES b/CHANGES index a451b978..6727e98f 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,9 @@ Relase date to be decided, codename to be chosen. - Added the ``APPLICATION_ROOT`` configuration variable. - Implemented :meth:`~flask.testing.TestClient.session_transaction` to easily modify sessions from the test environment. +- Refactored test client internally. The ``APPLICATION_ROOT`` configuration + variable as well as ``SERVER_NAME`` are now properly used by the test client + as defaults. Version 0.7.3 ------------- diff --git a/docs/api.rst b/docs/api.rst index f4fab86f..1dc25eb1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -223,7 +223,7 @@ Test Client .. currentmodule:: flask.testing -.. autoclass:: TestClient +.. autoclass:: FlaskClient :members: diff --git a/flask/app.py b/flask/app.py index 20cbca52..b4ae64a5 100644 --- a/flask/app.py +++ b/flask/app.py @@ -706,7 +706,7 @@ class Flask(_PackageBoundObject): rv = c.get('/?vodka=42') assert request.args['vodka'] == '42' - See :class:`~flask.testing.TestClient` for more information. + See :class:`~flask.testing.FlaskClient` for more information. .. versionchanged:: 0.4 added support for `with` block usage for the client. @@ -1481,19 +1481,12 @@ class Flask(_PackageBoundObject): :func:`werkzeug.test.EnvironBuilder` for more information, this function accepts the same arguments). """ - from werkzeug.test import create_environ - environ_overrides = kwargs.setdefault('environ_overrides', {}) - if self.config.get('SERVER_NAME'): - server_name = self.config.get('SERVER_NAME') - if ':' not in server_name: - http_host, http_port = server_name, '80' - else: - http_host, http_port = server_name.split(':', 1) - - environ_overrides.setdefault('SERVER_NAME', server_name) - environ_overrides.setdefault('HTTP_HOST', server_name) - environ_overrides.setdefault('SERVER_PORT', http_port) - return self.request_context(create_environ(*args, **kwargs)) + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() def wsgi_app(self, environ, start_response): """The actual WSGI application. This is not implemented in diff --git a/flask/testing.py b/flask/testing.py index dfdaff57..c4d18ca2 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -15,6 +15,17 @@ from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): + """Creates a new test builder with some application defaults thrown in.""" + http_host = app.config.get('SERVER_NAME') + app_root = app.config.get('APPLICATION_ROOT') + if base_url is None: + base_url = 'http://%s/' % (http_host or 'localhost') + if app_root: + base_url += app_root.lstrip('/') + return EnvironBuilder(path, base_url, *args, **kwargs) + + class FlaskClient(Client): """Works like a regular Werkzeug test client but has some knowledge about how Flask works to defer the cleanup of the request context stack to the @@ -83,21 +94,8 @@ class FlaskClient(Client): as_tuple = kwargs.pop('as_tuple', False) buffered = kwargs.pop('buffered', False) follow_redirects = kwargs.pop('follow_redirects', False) + builder = make_test_environ_builder(self.application, *args, **kwargs) - builder = EnvironBuilder(*args, **kwargs) - - if self.application.config.get('SERVER_NAME'): - server_name = self.application.config.get('SERVER_NAME') - if ':' not in server_name: - http_host, http_port = server_name, None - else: - http_host, http_port = server_name.split(':', 1) - if builder.base_url == 'http://localhost/': - # Default Generated Base URL - if http_port != None: - builder.host = http_host + ':' + http_port - else: - builder.host = http_host old = _request_ctx_stack.top try: return Client.open(self, builder, diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 62814882..aa8f2cf3 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1042,6 +1042,21 @@ class BasicFunctionalityTestCase(FlaskTestCase): class TestToolsTestCase(FlaskTestCase): + def test_environ_defaults_from_config(self): + app = flask.Flask(__name__) + app.testing = True + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assertEqual(ctx.request.url, 'http://example.com:1234/foo/') + with app.test_client() as c: + rv = c.get('/') + self.assertEqual(rv.data, 'http://example.com:1234/foo/') + def test_session_transactions(self): app = flask.Flask(__name__) app.testing = True From 8dbd71ef8e16ce64cfcf7c526d584ea2771a28d1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 20:48:38 +0100 Subject: [PATCH 0780/3747] Added a testcase where SERVER_NAME and APPLICATION_ROOT are not set --- tests/flask_tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index aa8f2cf3..040ddfaa 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1057,6 +1057,19 @@ class TestToolsTestCase(FlaskTestCase): rv = c.get('/') self.assertEqual(rv.data, 'http://example.com:1234/foo/') + def test_environ_defaults(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assertEqual(ctx.request.url, 'http://localhost/') + with app.test_client() as c: + rv = c.get('/') + self.assertEqual(rv.data, 'http://localhost/') + def test_session_transactions(self): app = flask.Flask(__name__) app.testing = True From 485a6c332b1cd10495f6f3aafe2690e07f01b422 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 20:49:53 +0100 Subject: [PATCH 0781/3747] Moved testcase for test client context binding to the TestToolsTestCase --- tests/flask_tests.py | 60 ++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 040ddfaa..1d9daf83 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -153,36 +153,6 @@ class ContextTestCase(FlaskTestCase): else: assert 0, 'expected runtime error' - def test_test_client_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - flask.g.value = 42 - return 'Hello World!' - - @app.route('/other') - def other(): - 1/0 - - with app.test_client() as c: - resp = c.get('/') - assert flask.g.value == 42 - assert resp.data == 'Hello World!' - assert resp.status_code == 200 - - resp = c.get('/other') - assert not hasattr(flask.g, 'value') - assert 'Internal Server Error' in resp.data - assert resp.status_code == 500 - flask.g.value = 23 - - try: - flask.g.value - except (AttributeError, RuntimeError): - pass - else: - raise AssertionError('some kind of exception expected') - class BasicFunctionalityTestCase(FlaskTestCase): @@ -1111,6 +1081,36 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction(): self.assert_(req is flask.request._get_current_object()) + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + assert flask.g.value == 42 + assert resp.data == 'Hello World!' + assert resp.status_code == 200 + + resp = c.get('/other') + assert not hasattr(flask.g, 'value') + assert 'Internal Server Error' in resp.data + assert resp.status_code == 500 + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + class InstanceTestCase(FlaskTestCase): From d3ca55177a9f39de526143c59edc049d28f4424c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 20:56:43 +0100 Subject: [PATCH 0782/3747] Updated the docs and examples to non-failing teardown handlers --- docs/patterns/sqlite3.rst | 10 +++++++++- docs/reqcontext.rst | 10 +++++++++- docs/upgrading.rst | 3 ++- examples/flaskr/flaskr.py | 3 ++- examples/minitwit/minitwit.py | 3 ++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index bc471f66..0d02e465 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -24,7 +24,15 @@ So here is a simple example of how you can use SQLite 3 with Flask:: @app.teardown_request def teardown_request(exception): - g.db.close() + if hasattr(g, 'db'): + g.db.close() + +.. note:: + + Please keep in mind that the teardown request functions are always + executed, even if a before-request handler failed or was never + executed. Because of this we have to make sure here that the database + is there before we close it. Connect on Demand ----------------- diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 3b49e1d5..0249b88e 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -131,7 +131,9 @@ understand what is actually happening. The new behavior is quite simple: 4. At the end of the request the :meth:`~flask.Flask.teardown_request` functions are executed. This always happens, even in case of an - unhandled exception down the road. + unhandled exception down the road or if a before-request handler was + not executed yet or at all (for example in test environments sometimes + you might want to not execute before-request callbacks). Now what happens on errors? In production mode if an exception is not caught, the 500 internal server handler is called. In development mode @@ -183,6 +185,12 @@ It's easy to see the behavior from the command line: this runs after request >>> +Keep in mind that teardown callbacks are always executed, even if +before-request callbacks were not executed yet but an exception happened. +Certain parts of the test system might also temporarily create a request +context without calling the before-request handlers. Make sure to write +your teardown-request handlers in a way that they will never fail. + .. _notes-on-proxies: Notes On Proxies diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 13b5be71..b318292c 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -142,7 +142,8 @@ You are now encouraged to use this instead:: @app.teardown_request def after_request(exception): - g.db.close() + if hasattr(g, 'db'): + g.db.close() On the upside this change greatly improves the internal code flow and makes it easier to customize the dispatching and error handling. This diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 361c1aee..6f9b06fc 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -50,7 +50,8 @@ def before_request(): @app.teardown_request def teardown_request(exception): """Closes the database again at the end of the request.""" - g.db.close() + if hasattr(g, 'db'): + g.db.close() @app.route('/') diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index f7c700d3..fee023fd 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -85,7 +85,8 @@ def before_request(): @app.teardown_request def teardown_request(exception): """Closes the database again at the end of the request.""" - g.db.close() + if hasattr(g, 'db'): + g.db.close() @app.route('/') From a43f73c75c169de86dde372d6ecc42019773271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuh=C3=A4user?= Date: Fri, 26 Aug 2011 10:13:54 +0300 Subject: [PATCH 0783/3747] s/Ify ou/If you/ typo --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index ed44e5a2..1e00fe80 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -281,7 +281,7 @@ Accessing and Modifying Sessions .. versionadded:: 0.8 Sometimes it can be very helpful to access or modify the sessions from the -test client. Generally there are two ways for this. Ify ou just want to +test client. Generally there are two ways for this. If you just want to ensure that a session has certain keys set to certain values you can just keep the context around and access :data:`flask.session`:: From 314f9201ab133ca7b78f219937371223e14ee32a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 21:16:06 +0100 Subject: [PATCH 0784/3747] Updated instance path documentation to explain the $PREFIX lookup --- docs/config.rst | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 65e5064a..df31aba0 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -299,25 +299,37 @@ With Flask 0.8 a new attribute was introduced: version control and be deployment specific. It's the perfect place to drop things that either change at runtime or configuration files. -To make it easier to put this folder into an ignore list for your version -control system it's called ``instance`` and placed directly next to your -package or module by default. This path can be overridden by specifying -the `instance_path` parameter to your application:: +You can either explicitly provide the path of the instance folder when +creating the Flask application or you can let Flask autodetect the +instance folder. For explicit configuration use the `instance_path` +parameter:: app = Flask(__name__, instance_path='/path/to/instance/folder') -Default locations:: +Please keep in mind that this path *must* be absolute when provided. + +If the `instance_path` parameter is not provided the following default +locations are used: + +- Uninstalled module:: - Module situation: /myapp.py /instance - Package situation: +- Uninstalled package:: + /myapp /__init__.py /instance -Please keep in mind that this path *must* be absolute when provided. +- Installed module or package:: + + $PREFIX/lib/python2.X/site-packages/myapp + $PREFIX/var/myapp-instance + + ``$PREFIX`` is the prefix of your Python installation. This can be + ``/usr`` or the path to your virtualenv. You can print the value of + ``sys.prefix`` to see what the prefix is set to. Since the config object provided loading of configuration files from relative filenames we made it possible to change the loading via filenames From 8340d3c9f5cb38f23dd16d7d28aa643a121c4236 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 21:49:50 +0100 Subject: [PATCH 0785/3747] Updated docstring on make_response --- flask/helpers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index a260b03f..d8f7ac63 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -145,6 +145,13 @@ def make_response(*args): response = make_response(render_template('not_found.html'), 404) + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + Internally this function does the following things: - if no arguments are passed, it creates a new response argument From ef0f626f0a916fdced42e05aa2f58dede6c660fd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Aug 2011 22:09:48 +0100 Subject: [PATCH 0786/3747] Added flask.views.View.decorators to automatically decorate class based views. --- CHANGES | 2 ++ flask/views.py | 35 +++++++++++++++++++++++++++++++++++ tests/flask_tests.py | 21 +++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/CHANGES b/CHANGES index 6727e98f..fa8c00bf 100644 --- a/CHANGES +++ b/CHANGES @@ -38,6 +38,8 @@ Relase date to be decided, codename to be chosen. - Refactored test client internally. The ``APPLICATION_ROOT`` configuration variable as well as ``SERVER_NAME`` are now properly used by the test client as defaults. +- Added :attr:`flask.views.View.decorators` to support simpler decorating of + pluggable (class based) views. Version 0.7.3 ------------- diff --git a/flask/views.py b/flask/views.py index 9a185570..2fe34622 100644 --- a/flask/views.py +++ b/flask/views.py @@ -30,10 +30,38 @@ class View(object): return 'Hello %s!' % name app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + + When you want to decorate a pluggable view you will have to either do that + when the view function is created (by wrapping the return value of + :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + + class SecretView(View): + methods = ['GET'] + decorators = [superuser_required] + + def dispatch_request(self): + ... + + The decorators stored in the decorators list are applied one after another + when the view function is created. Note that you can *not* use the class + based decorators since those would decorate the view class and not the + generated view function! """ + #: A for which methods this pluggable view can handle. methods = None + #: The canonical way to decorate class based views is to decorate the + #: return value of as_view(). However since this moves parts of the + #: logic from the class declaration to the place where it's hooked + #: into the routing system. + #: + #: You can place one or more decorators in this list and whenever the + #: view function is created the result is automatically decorated. + #: + #: .. versionadded:: 0.8 + decorators = [] + def dispatch_request(self): """Subclasses have to override this method to implement the actual view functionc ode. This method is called with all @@ -54,6 +82,13 @@ class View(object): def view(*args, **kwargs): self = view.view_class(*class_args, **class_kwargs) return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + # we attach the view class to the view function for two reasons: # first of all it allows us to easily figure out what class based # view this thing came from, secondly it's also used for instanciating diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 1d9daf83..dcc3e1a0 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -2258,6 +2258,27 @@ class ViewTestCase(FlaskTestCase): meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + def test_view_decorators(self): + app = flask.Flask(__name__) + + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers['X-Parachute'] = 'awesome' + return resp + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + def dispatch_request(self): + return 'Awesome' + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assertEqual(rv.headers['X-Parachute'], 'awesome') + self.assertEqual(rv.data, 'Awesome') + class DeprecationsTestCase(FlaskTestCase): From 85ed1bf05834f484ce7dfdc37e11b89eec8cee65 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 08:32:36 +0100 Subject: [PATCH 0787/3747] Mentioned View.decorators in the views docs --- docs/views.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/views.rst b/docs/views.rst index fc22d1d7..10ddb57d 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -135,3 +135,24 @@ easily do that. Each HTTP method maps to a function with the same name That way you also don't have to provide the :attr:`~flask.views.View.methods` attribute. It's automatically set based on the methods defined in the class. + +Decorating Views +---------------- + +Since the view class itself is not the view function that is added to the +routing system it does not make much sense to decorate the class itself. +Instead you either have to decorate the return value of +:meth:`~flask.views.View.as_view` by hand:: + + view = rate_limited(UserAPI.as_view('users')) + app.add_url_rule('/users/', view_func=view) + +Starting with Flask 0.8 there is also an alternative way where you can +specify a list of decorators to apply in the class declaration:: + + class UserAPI(MethodView): + decorators = [rate_limited] + +Due to the implicit self from the caller's perspective you cannot use +regular view decorators on the individual methods of the view however, +keep this in mind. From 4cb6eea8f1e1edc707874f72cbca43a0f698ec7d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:21:26 +0100 Subject: [PATCH 0788/3747] Split up testsuite and moved it to flask.testsuite. This fixes #246 --- flask/testsuite/__init__.py | 121 + flask/testsuite/basic.py | 992 +++++++ flask/testsuite/blueprints.py | 423 +++ flask/testsuite/config.py | 177 ++ flask/testsuite/deprecations.py | 38 + flask/testsuite/examples.py | 38 + flask/testsuite/helpers.py | 295 +++ flask/testsuite/signals.py | 103 + {tests => flask/testsuite}/static/index.html | 0 .../testsuite}/templates/_macro.html | 0 .../templates/context_template.html | 0 .../templates/escaping_template.html | 0 {tests => flask/testsuite}/templates/mail.txt | 0 .../testsuite}/templates/nested/nested.txt | 0 .../testsuite}/templates/simple_template.html | 0 .../testsuite}/templates/template_filter.html | 0 flask/testsuite/templating.py | 141 + .../test_apps}/blueprintapp/__init__.py | 0 .../test_apps}/blueprintapp/apps/__init__.py | 0 .../blueprintapp/apps/admin/__init__.py | 0 .../apps/admin/static/css/test.css | 0 .../blueprintapp/apps/admin/static/test.txt | 0 .../apps/admin/templates/admin/index.html | 0 .../blueprintapp/apps/frontend/__init__.py | 0 .../frontend/templates/frontend/index.html | 0 .../testsuite/test_apps/config_module_app.py | 4 + .../test_apps/config_package_app/__init__.py | 4 + .../test_apps}/moduleapp/__init__.py | 0 .../test_apps}/moduleapp/apps/__init__.py | 0 .../moduleapp/apps/admin/__init__.py | 0 .../moduleapp/apps/admin/static/css/test.css | 0 .../moduleapp/apps/admin/static/test.txt | 0 .../moduleapp/apps/admin/templates/index.html | 0 .../moduleapp/apps/frontend/__init__.py | 0 .../apps/frontend/templates/index.html | 0 .../subdomaintestmodule/__init__.py | 0 .../subdomaintestmodule/static/hello.txt | 0 flask/testsuite/testing.py | 121 + flask/testsuite/views.py | 117 + {tests => scripts}/flaskext_test.py | 0 setup.py | 8 +- tests/flask_tests.py | 2329 ----------------- 42 files changed, 2575 insertions(+), 2336 deletions(-) create mode 100644 flask/testsuite/__init__.py create mode 100644 flask/testsuite/basic.py create mode 100644 flask/testsuite/blueprints.py create mode 100644 flask/testsuite/config.py create mode 100644 flask/testsuite/deprecations.py create mode 100644 flask/testsuite/examples.py create mode 100644 flask/testsuite/helpers.py create mode 100644 flask/testsuite/signals.py rename {tests => flask/testsuite}/static/index.html (100%) rename {tests => flask/testsuite}/templates/_macro.html (100%) rename {tests => flask/testsuite}/templates/context_template.html (100%) rename {tests => flask/testsuite}/templates/escaping_template.html (100%) rename {tests => flask/testsuite}/templates/mail.txt (100%) rename {tests => flask/testsuite}/templates/nested/nested.txt (100%) rename {tests => flask/testsuite}/templates/simple_template.html (100%) rename {tests => flask/testsuite}/templates/template_filter.html (100%) create mode 100644 flask/testsuite/templating.py rename {tests => flask/testsuite/test_apps}/blueprintapp/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/admin/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/admin/static/css/test.css (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/admin/static/test.txt (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/admin/templates/admin/index.html (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/frontend/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/blueprintapp/apps/frontend/templates/frontend/index.html (100%) create mode 100644 flask/testsuite/test_apps/config_module_app.py create mode 100644 flask/testsuite/test_apps/config_package_app/__init__.py rename {tests => flask/testsuite/test_apps}/moduleapp/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/admin/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/admin/static/css/test.css (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/admin/static/test.txt (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/admin/templates/index.html (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/frontend/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/moduleapp/apps/frontend/templates/index.html (100%) rename {tests => flask/testsuite/test_apps}/subdomaintestmodule/__init__.py (100%) rename {tests => flask/testsuite/test_apps}/subdomaintestmodule/static/hello.txt (100%) create mode 100644 flask/testsuite/testing.py create mode 100644 flask/testsuite/views.py rename {tests => scripts}/flaskext_test.py (100%) delete mode 100644 tests/flask_tests.py diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py new file mode 100644 index 00000000..8df7a7fd --- /dev/null +++ b/flask/testsuite/__init__.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite + ~~~~~~~~~~~~~~~ + + Tests Flask itself. The majority of Flask is already tested + as part of Werkzeug. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import warnings +import unittest +from StringIO import StringIO +from functools import update_wrapper +from contextlib import contextmanager +from werkzeug.utils import import_string, find_modules + + +def add_to_path(path): + def _samefile(x, y): + try: + return os.path.samefile(x, y) + except (IOError, OSError): + return False + for entry in sys.path: + try: + if os.path.samefile(path, entry): + return + except (OSError, IOError): + pass + sys.path.append(path) + + +def setup_paths(): + add_to_path(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps'))) + + +def iter_suites(): + for module in find_modules(__name__): + mod = import_string(module) + if hasattr(mod, 'suite'): + yield mod.suite() + + +@contextmanager +def catch_warnings(): + """Catch warnings in a with block in a list""" + # make sure deprecation warnings are active in tests + warnings.simplefilter('default', category=DeprecationWarning) + + filters = warnings.filters + warnings.filters = filters[:] + old_showwarning = warnings.showwarning + log = [] + def showwarning(message, category, filename, lineno, file=None, line=None): + log.append(locals()) + try: + warnings.showwarning = showwarning + yield log + finally: + warnings.filters = filters + warnings.showwarning = old_showwarning + + +@contextmanager +def catch_stderr(): + """Catch stderr in a StringIO""" + old_stderr = sys.stderr + sys.stderr = rv = StringIO() + try: + yield rv + finally: + sys.stderr = old_stderr + + +def emits_module_deprecation_warning(f): + def new_f(self, *args, **kwargs): + with catch_warnings() as log: + f(self, *args, **kwargs) + self.assert_(log, 'expected deprecation warning') + for entry in log: + self.assert_('Modules are deprecated' in str(entry['message'])) + return update_wrapper(new_f, f) + + +class FlaskTestCase(unittest.TestCase): + + def ensure_clean_request_context(self): + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + self.assert_equal(flask._request_ctx_stack.top, None) + + def setup(self): + pass + + def teardown(self): + pass + + def setUp(self): + self.setup() + + def tearDown(self): + unittest.TestCase.tearDown(self) + self.ensure_clean_request_context() + self.teardown() + + def assert_equal(self, x, y): + return self.assertEqual(x, y) + + +def suite(): + setup_paths() + suite = unittest.TestSuite() + for other_suite in iter_suites(): + suite.addTest(other_suite) + return suite diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py new file mode 100644 index 00000000..c55881e5 --- /dev/null +++ b/flask/testsuite/basic.py @@ -0,0 +1,992 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.basic + ~~~~~~~~~~~~~~~~~~~~~ + + The basic functionality. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import re +import flask +import unittest +from datetime import datetime +from threading import Thread +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.http import parse_date + + +class BasicFunctionalityTestCase(FlaskTestCase): + + def test_options_work(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + assert rv.data == '' + + def test_options_on_multiple_rules(self): + app = flask.Flask(__name__) + @app.route('/', methods=['GET', 'POST']) + def index(): + return 'Hello World' + @app.route('/', methods=['PUT']) + def index_put(): + return 'Aha!' + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + + def test_options_handling_disabled(self): + app = flask.Flask(__name__) + def index(): + return 'Hello World!' + index.provide_automatic_options = False + app.route('/')(index) + rv = app.test_client().open('/', method='OPTIONS') + assert rv.status_code == 405 + + app = flask.Flask(__name__) + def index2(): + return 'Hello World!' + index2.provide_automatic_options = True + app.route('/', methods=['OPTIONS'])(index2) + rv = app.test_client().open('/', method='OPTIONS') + assert sorted(rv.allow) == ['OPTIONS'] + + def test_request_dispatching(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.request.method + @app.route('/more', methods=['GET', 'POST']) + def more(): + return flask.request.method + + c = app.test_client() + assert c.get('/').data == 'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == 'POST' + assert c.get('/more').data == 'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + + def test_url_mapping(self): + app = flask.Flask(__name__) + def index(): + return flask.request.method + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) + + c = app.test_client() + assert c.get('/').data == 'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == 'POST' + assert c.get('/more').data == 'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + + def test_werkzeug_routing(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + def bar(): + return 'bar' + def index(): + return 'index' + app.view_functions['bar'] = bar + app.view_functions['index'] = index + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + app = flask.Flask(__name__) + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + + @app.endpoint('bar') + def bar(): + return 'bar' + + @app.endpoint('index') + def index(): + return 'index' + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + + def test_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/set', methods=['POST']) + def set(): + flask.session['value'] = flask.request.form['value'] + return 'value set' + @app.route('/get') + def get(): + return flask.session['value'] + + c = app.test_client() + assert c.post('/set', data={'value': '42'}).data == 'value set' + assert c.get('/get').data == '42' + + def test_session_using_server_name(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com/') + assert 'domain=.example.com' in rv.headers['set-cookie'].lower() + assert 'httponly' in rv.headers['set-cookie'].lower() + + def test_session_using_server_name_and_port(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + assert 'domain=.example.com' in rv.headers['set-cookie'].lower() + assert 'httponly' in rv.headers['set-cookie'].lower() + + def test_session_using_application_root(self): + class PrefixPathMiddleware(object): + def __init__(self, app, prefix): + self.app = app + self.prefix = prefix + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = self.prefix + return self.app(environ, start_response) + + app = flask.Flask(__name__) + app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') + app.config.update( + SECRET_KEY='foo', + APPLICATION_ROOT='/bar' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/') + assert 'path=/bar' in rv.headers['set-cookie'].lower() + + def test_missing_session(self): + app = flask.Flask(__name__) + def expect_exception(f, *args, **kwargs): + try: + f(*args, **kwargs) + except RuntimeError, e: + assert e.args and 'session is unavailable' in e.args[0] + else: + assert False, 'expected exception' + with app.test_request_context(): + assert flask.session.get('missing_key') is None + expect_exception(flask.session.__setitem__, 'foo', 42) + expect_exception(flask.session.pop, 'foo') + + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + + @app.route('/test') + def test(): + return unicode(flask.session.permanent) + + client = app.test_client() + rv = client.get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + assert expires.year == expected.year + assert expires.month == expected.month + assert expires.day == expected.day + + rv = client.get('/test') + assert rv.data == 'True' + + permanent = False + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + assert match is None + + def test_flashes(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + with app.test_request_context(): + assert not flask.session.modified + flask.flash('Zap') + flask.session.modified = False + flask.flash('Zip') + assert flask.session.modified + assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] + + def test_extended_flashing(self): + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/') + def index(): + flask.flash(u'Hello World') + flask.flash(u'Hello World', 'error') + flask.flash(flask.Markup(u'Testing'), 'warning') + return '' + + @app.route('/test') + def test(): + messages = flask.get_flashed_messages(with_categories=True) + assert len(messages) == 3 + assert messages[0] == ('message', u'Hello World') + assert messages[1] == ('error', u'Hello World') + assert messages[2] == ('warning', flask.Markup(u'Testing')) + return '' + messages = flask.get_flashed_messages() + assert len(messages) == 3 + assert messages[0] == u'Hello World' + assert messages[1] == u'Hello World' + assert messages[2] == flask.Markup(u'Testing') + + c = app.test_client() + c.get('/') + c.get('/test') + + def test_request_processing(self): + app = flask.Flask(__name__) + evts = [] + @app.before_request + def before_request(): + evts.append('before') + @app.after_request + def after_request(response): + response.data += '|after' + evts.append('after') + return response + @app.route('/') + def index(): + assert 'before' in evts + assert 'after' not in evts + return 'request' + assert 'after' not in evts + rv = app.test_client().get('/').data + assert 'after' in evts + assert rv == 'request|after' + + def test_teardown_request_handler(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert 'Response' in rv.data + assert len(called) == 1 + + def test_teardown_request_handler_debug_mode(self): + called = [] + app = flask.Flask(__name__) + app.testing = True + @app.teardown_request + def teardown_request(exc): + called.append(True) + return "Ignored" + @app.route('/') + def root(): + return "Response" + rv = app.test_client().get('/') + assert rv.status_code == 200 + assert 'Response' in rv.data + assert len(called) == 1 + + def test_teardown_request_handler_error(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_request1(exc): + assert type(exc) == ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.teardown_request + def teardown_request2(exc): + assert type(exc) == ZeroDivisionError + called.append(True) + # This raises a new error and blows away sys.exc_info(), so we can + # test that all teardown_requests get passed the same original + # exception. + try: + raise TypeError + except: + pass + @app.route('/') + def fails(): + 1/0 + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + assert len(called) == 2 + + def test_before_after_request_order(self): + called = [] + app = flask.Flask(__name__) + @app.before_request + def before1(): + called.append(1) + @app.before_request + def before2(): + called.append(2) + @app.after_request + def after1(response): + called.append(4) + return response + @app.after_request + def after2(response): + called.append(3) + return response + @app.teardown_request + def finish1(exc): + called.append(6) + @app.teardown_request + def finish2(exc): + called.append(5) + @app.route('/') + def index(): + return '42' + rv = app.test_client().get('/') + assert rv.data == '42' + assert called == [1, 2, 3, 4, 5, 6] + + def test_error_handling(self): + app = flask.Flask(__name__) + @app.errorhandler(404) + def not_found(e): + return 'not found', 404 + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @app.route('/') + def index(): + flask.abort(404) + @app.route('/error') + def error(): + 1 // 0 + c = app.test_client() + rv = c.get('/') + assert rv.status_code == 404 + assert rv.data == 'not found' + rv = c.get('/error') + assert rv.status_code == 500 + assert 'internal server error' == rv.data + + def test_before_request_and_routing_errors(self): + app = flask.Flask(__name__) + @app.before_request + def attach_something(): + flask.g.something = 'value' + @app.errorhandler(404) + def return_something(error): + return flask.g.something, 404 + rv = app.test_client().get('/') + assert rv.status_code == 404 + assert rv.data == 'value' + + def test_user_error_handling(self): + class MyException(Exception): + pass + + app = flask.Flask(__name__) + @app.errorhandler(MyException) + def handle_my_exception(e): + assert isinstance(e, MyException) + return '42' + @app.route('/') + def index(): + raise MyException() + + c = app.test_client() + assert c.get('/').data == '42' + + def test_trapping_of_bad_request_key_errors(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/fail') + def fail(): + flask.request.form['missing_key'] + c = app.test_client() + assert c.get('/fail').status_code == 400 + + app.config['TRAP_BAD_REQUEST_ERRORS'] = True + c = app.test_client() + try: + c.get('/fail') + except KeyError, e: + assert isinstance(e, BadRequest) + else: + self.fail('Expected exception') + + def test_trapping_of_all_http_exceptions(self): + app = flask.Flask(__name__) + app.testing = True + app.config['TRAP_HTTP_EXCEPTIONS'] = True + @app.route('/fail') + def fail(): + flask.abort(404) + + c = app.test_client() + try: + c.get('/fail') + except NotFound, e: + pass + else: + self.fail('Expected exception') + + def test_enctype_debug_helper(self): + from flask.debughelpers import DebugFilesKeyError + app = flask.Flask(__name__) + app.debug = True + @app.route('/fail', methods=['POST']) + def index(): + return flask.request.files['foo'].filename + + # with statement is important because we leave an exception on the + # stack otherwise and we want to ensure that this is not the case + # to not negatively affect other tests. + with app.test_client() as c: + try: + c.post('/fail', data={'foo': 'index.txt'}) + except DebugFilesKeyError, e: + assert 'no file contents were transmitted' in str(e) + assert 'This was submitted: "index.txt"' in str(e) + else: + self.fail('Expected exception') + + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + assert buffer == [] + ctx.pop() + assert buffer == [None] + + def test_response_creation(self): + app = flask.Flask(__name__) + @app.route('/unicode') + def from_unicode(): + return u'Hällo Wörld' + @app.route('/string') + def from_string(): + return u'Hällo Wörld'.encode('utf-8') + @app.route('/args') + def from_tuple(): + return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + c = app.test_client() + assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') + assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') + rv = c.get('/args') + assert rv.data == 'Meh' + assert rv.headers['X-Foo'] == 'Testing' + assert rv.status_code == 400 + assert rv.mimetype == 'text/plain' + + def test_make_response(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response() + assert rv.status_code == 200 + assert rv.data == '' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('Awesome') + assert rv.status_code == 200 + assert rv.data == 'Awesome' + assert rv.mimetype == 'text/html' + + rv = flask.make_response('W00t', 404) + assert rv.status_code == 404 + assert rv.data == 'W00t' + assert rv.mimetype == 'text/html' + + def test_url_generation(self): + app = flask.Flask(__name__) + @app.route('/hello/', methods=['POST']) + def hello(): + pass + with app.test_request_context(): + assert flask.url_for('hello', name='test x') == '/hello/test%20x' + assert flask.url_for('hello', name='test x', _external=True) \ + == 'http://localhost/hello/test%20x' + + def test_custom_converters(self): + from werkzeug.routing import BaseConverter + class ListConverter(BaseConverter): + def to_python(self, value): + return value.split(',') + def to_url(self, value): + base_to_url = super(ListConverter, self).to_url + return ','.join(base_to_url(x) for x in value) + app = flask.Flask(__name__) + app.url_map.converters['list'] = ListConverter + @app.route('/') + def index(args): + return '|'.join(args) + c = app.test_client() + assert c.get('/1,2,3').data == '1|2|3' + + def test_static_files(self): + app = flask.Flask(__name__) + rv = app.test_client().get('/static/index.html') + assert rv.status_code == 200 + assert rv.data.strip() == '

    Hello World!

    ' + with app.test_request_context(): + assert flask.url_for('static', filename='index.html') \ + == '/static/index.html' + + def test_none_response(self): + app = flask.Flask(__name__) + @app.route('/') + def test(): + return None + try: + app.test_client().get('/') + except ValueError, e: + assert str(e) == 'View function did not return a response' + pass + else: + assert "Expected ValueError" + + def test_request_locals(self): + self.assert_equal(repr(flask.g), '') + self.assertFalse(flask.g) + + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + + @app.route('/') + def index(): + return None + + @app.route('/', subdomain='foo') + def sub(): + return None + + with app.test_request_context('/'): + assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' + + with app.test_request_context('/'): + assert flask.url_for('sub', _external=True) == 'http://foo.localhost.localdomain:5000/' + + try: + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + except Exception, e: + assert isinstance(e, ValueError) + assert str(e) == "the server name provided " + \ + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')", str(e) + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_test_app_proper_environ(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + @app.route('/') + def index(): + return 'Foo' + + @app.route('/', subdomain='foo') + def subdomain(): + return 'Foo SubDomain' + + try: + rv = app.test_client().get('/') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + assert rv.data == 'Foo' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost.localdomain:443') + rv = app.test_client().get('/', 'https://localhost.localdomain') + assert rv.data == 'Foo' + except ValueError, e: + assert str(e) == "the server name provided " + \ + "('localhost.localdomain:443') does not match the " + \ + "server name from the WSGI environment ('localhost.localdomain')", str(e) + + try: + app.config.update(SERVER_NAME='localhost.localdomain') + app.test_client().get('/', 'http://foo.localhost') + except ValueError, e: + assert str(e) == "the server name provided " + \ + "('localhost.localdomain') does not match the " + \ + "server name from the WSGI environment ('foo.localhost')", str(e) + + try: + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + assert rv.data == 'Foo SubDomain' + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_exception_propagation(self): + def apprunner(configkey): + app = flask.Flask(__name__) + @app.route('/') + def index(): + 1/0 + c = app.test_client() + if config_key is not None: + app.config[config_key] = True + try: + resp = c.get('/') + except Exception: + pass + else: + self.fail('expected exception') + else: + assert c.get('/').status_code == 500 + + # we have to run this test in an isolated thread because if the + # debug flag is set to true and an exception happens the context is + # not torn down. This causes other tests that run after this fail + # when they expect no exception on the stack. + for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: + t = Thread(target=apprunner, args=(config_key,)) + t.start() + t.join() + + def test_max_content_length(self): + app = flask.Flask(__name__) + app.config['MAX_CONTENT_LENGTH'] = 64 + @app.before_request + def always_first(): + flask.request.form['myfile'] + assert False + @app.route('/accept', methods=['POST']) + def accept_file(): + flask.request.form['myfile'] + assert False + @app.errorhandler(413) + def catcher(error): + return '42' + + c = app.test_client() + rv = c.post('/accept', data={'myfile': 'foo' * 100}) + assert rv.data == '42' + + def test_url_processors(self): + app = flask.Flask(__name__) + + @app.url_defaults + def add_language_code(endpoint, values): + if flask.g.lang_code is not None and \ + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): + values.setdefault('lang_code', flask.g.lang_code) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code', None) + + @app.route('//') + def index(): + return flask.url_for('about') + + @app.route('//about') + def about(): + return flask.url_for('something_else') + + @app.route('/foo') + def something_else(): + return flask.url_for('about', lang_code='en') + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/foo') + self.assert_equal(c.get('/foo').data, '/en/about') + + def test_debug_mode_complains_after_first_request(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/') + def index(): + return 'Awesome' + self.assert_(not app.got_first_request) + self.assert_equal(app.test_client().get('/').data, 'Awesome') + try: + @app.route('/foo') + def broken(): + return 'Meh' + except AssertionError, e: + self.assert_('A setup function was called' in str(e)) + else: + self.fail('Expected exception') + + app.debug = False + @app.route('/foo') + def working(): + return 'Meh' + self.assert_equal(app.test_client().get('/foo').data, 'Meh') + self.assert_(app.got_first_request) + + def test_before_first_request_functions(self): + got = [] + app = flask.Flask(__name__) + @app.before_first_request + def foo(): + got.append(42) + c = app.test_client() + c.get('/') + self.assert_equal(got, [42]) + c.get('/') + self.assert_equal(got, [42]) + self.assert_(app.got_first_request) + + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assert_equal(rv.data, 'success') + + +class ContextTestCase(FlaskTestCase): + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + assert index() == 'Hello World!' + with app.test_request_context('/meh'): + assert meh() == 'http://localhost/meh' + assert flask._request_ctx_stack.top is None + + def test_context_test(self): + app = flask.Flask(__name__) + assert not flask.request + assert not flask.has_request_context() + ctx = app.test_request_context() + ctx.push() + try: + assert flask.request + assert flask.has_request_context() + finally: + ctx.pop() + + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + assert index() == 'Hello World!' + ctx.pop() + try: + index() + except RuntimeError: + pass + else: + assert 0, 'expected runtime error' + + +class SubdomainTestCase(FlaskTestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/') + def normal_index(): + return 'normal index' + @app.route('/', subdomain='test') + def test_index(): + return 'test index' + + c = app.test_client() + rv = c.get('/', 'http://localhost/') + assert rv.data == 'normal index' + + rv = c.get('/', 'http://test.localhost/') + assert rv.data == 'test index' + + @emits_module_deprecation_warning + def test_module_static_path_subdomain(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'example.com' + from subdomaintestmodule import mod + app.register_module(mod) + c = app.test_client() + rv = c.get('/static/hello.txt', 'http://foo.example.com/') + assert rv.data.strip() == 'Hello Subdomain' + + def test_subdomain_matching(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost/') + assert rv.data == 'index for mitsuhiko' + + @emits_module_deprecation_warning + def test_module_subdomain_support(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'test', subdomain='testing') + app.config['SERVER_NAME'] = 'localhost' + + @mod.route('/test') + def test(): + return 'Test' + + @mod.route('/outside', subdomain='xtesting') + def bar(): + return 'Outside' + + app.register_module(mod) + + c = app.test_client() + rv = c.get('/test', 'http://testing.localhost/') + assert rv.data == 'Test' + rv = c.get('/outside', 'http://xtesting.localhost/') + assert rv.data == 'Outside' + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) + suite.addTest(unittest.makeSuite(ContextTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) + return suite diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py new file mode 100644 index 00000000..93122ac5 --- /dev/null +++ b/flask/testsuite/blueprints.py @@ -0,0 +1,423 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.blueprints + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints (and currently modules) + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +import warnings +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from werkzeug.exceptions import NotFound +from jinja2 import TemplateNotFound + + +# import moduleapp here because it uses deprecated features and we don't +# want to see the warnings +warnings.simplefilter('ignore', DeprecationWarning) +from moduleapp import app as moduleapp +warnings.simplefilter('default', DeprecationWarning) + + +class ModuleTestCase(FlaskTestCase): + + @emits_module_deprecation_warning + def test_basic_module(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.route('/') + def admin_index(): + return 'admin index' + @admin.route('/login') + def admin_login(): + return 'admin login' + @admin.route('/logout') + def admin_logout(): + return 'admin logout' + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + assert c.get('/').data == 'the index' + assert c.get('/admin/').data == 'admin index' + assert c.get('/admin/login').data == 'admin login' + assert c.get('/admin/logout').data == 'admin logout' + + @emits_module_deprecation_warning + def test_default_endpoint_name(self): + app = flask.Flask(__name__) + mod = flask.Module(__name__, 'frontend') + def index(): + return 'Awesome' + mod.add_url_rule('/', view_func=index) + app.register_module(mod) + rv = app.test_client().get('/') + assert rv.data == 'Awesome' + with app.test_request_context(): + assert flask.url_for('frontend.index') == '/' + + @emits_module_deprecation_warning + def test_request_processing(self): + catched = [] + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @admin.before_request + def before_admin_request(): + catched.append('before-admin') + @admin.after_request + def after_admin_request(response): + catched.append('after-admin') + return response + @admin.route('/') + def admin_index(): + return 'the admin' + @app.before_request + def before_request(): + catched.append('before-app') + @app.after_request + def after_request(response): + catched.append('after-app') + return response + @app.route('/') + def index(): + return 'the index' + app.register_module(admin) + c = app.test_client() + + assert c.get('/').data == 'the index' + assert catched == ['before-app', 'after-app'] + del catched[:] + + assert c.get('/admin/').data == 'the admin' + assert catched == ['before-app', 'before-admin', + 'after-admin', 'after-app'] + + @emits_module_deprecation_warning + def test_context_processors(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin', url_prefix='/admin') + @app.context_processor + def inject_all_regualr(): + return {'a': 1} + @admin.context_processor + def inject_admin(): + return {'b': 2} + @admin.app_context_processor + def inject_all_module(): + return {'c': 3} + @app.route('/') + def index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + @admin.route('/') + def admin_index(): + return flask.render_template_string('{{ a }}{{ b }}{{ c }}') + app.register_module(admin) + c = app.test_client() + assert c.get('/').data == '13' + assert c.get('/admin/').data == '123' + + @emits_module_deprecation_warning + def test_late_binding(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.route('/') + def index(): + return '42' + app.register_module(admin, url_prefix='/admin') + assert app.test_client().get('/admin/').data == '42' + + @emits_module_deprecation_warning + def test_error_handling(self): + app = flask.Flask(__name__) + admin = flask.Module(__name__, 'admin') + @admin.app_errorhandler(404) + def not_found(e): + return 'not found', 404 + @admin.app_errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + @admin.route('/') + def index(): + flask.abort(404) + @admin.route('/error') + def error(): + 1 // 0 + app.register_module(admin) + c = app.test_client() + rv = c.get('/') + assert rv.status_code == 404 + assert rv.data == 'not found' + rv = c.get('/error') + assert rv.status_code == 500 + assert 'internal server error' == rv.data + + def test_templates_and_static(self): + app = moduleapp + app.testing = True + c = app.test_client() + + rv = c.get('/') + assert rv.data == 'Hello from the Frontend' + rv = c.get('/admin/') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/static/test.txt') + assert rv.data.strip() == 'Admin File' + rv = c.get('/admin/static/css/test.css') + assert rv.data.strip() == '/* nested file */' + + with app.test_request_context(): + assert flask.url_for('admin.static', filename='test.txt') \ + == '/admin/static/test.txt' + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + assert e.name == 'missing.html' + else: + assert 0, 'expected exception' + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template('nested/nested.txt') == 'I\'m nested' + + def test_safe_access(self): + app = moduleapp + + with app.test_request_context(): + f = app.view_functions['admin.static'] + + try: + f('/etc/passwd') + except NotFound: + pass + else: + assert 0, 'expected exception' + try: + f('../__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + + # testcase for a security issue that may exist on windows systems + import os + import ntpath + old_path = os.path + os.path = ntpath + try: + try: + f('..\\__init__.py') + except NotFound: + pass + else: + assert 0, 'expected exception' + finally: + os.path = old_path + + @emits_module_deprecation_warning + def test_endpoint_decorator(self): + from werkzeug.routing import Submount, Rule + from flask import Module + + app = flask.Flask(__name__) + app.testing = True + app.url_map.add(Submount('/foo', [ + Rule('/bar', endpoint='bar'), + Rule('/', endpoint='index') + ])) + module = Module(__name__, __name__) + + @module.endpoint('bar') + def bar(): + return 'bar' + + @module.endpoint('index') + def index(): + return 'index' + + app.register_module(module) + + c = app.test_client() + assert c.get('/foo/').data == 'index' + assert c.get('/foo/bar').data == 'bar' + + +class BlueprintTestCase(FlaskTestCase): + + def test_blueprint_specific_error_handling(self): + frontend = flask.Blueprint('frontend', __name__) + backend = flask.Blueprint('backend', __name__) + sideend = flask.Blueprint('sideend', __name__) + + @frontend.errorhandler(403) + def frontend_forbidden(e): + return 'frontend says no', 403 + + @frontend.route('/frontend-no') + def frontend_no(): + flask.abort(403) + + @backend.errorhandler(403) + def backend_forbidden(e): + return 'backend says no', 403 + + @backend.route('/backend-no') + def backend_no(): + flask.abort(403) + + @sideend.route('/what-is-a-sideend') + def sideend_no(): + flask.abort(403) + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + app.register_blueprint(sideend) + + @app.errorhandler(403) + def app_forbidden(e): + return 'application itself says no', 403 + + c = app.test_client() + + assert c.get('/frontend-no').data == 'frontend says no' + assert c.get('/backend-no').data == 'backend says no' + assert c.get('/what-is-a-sideend').data == 'application itself says no' + + def test_blueprint_url_definitions(self): + bp = flask.Blueprint('test', __name__) + + @bp.route('/foo', defaults={'baz': 42}) + def foo(bar, baz): + return '%s/%d' % (bar, baz) + + @bp.route('/bar') + def bar(bar): + return unicode(bar) + + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) + app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) + + c = app.test_client() + self.assert_equal(c.get('/1/foo').data, u'23/42') + self.assert_equal(c.get('/2/foo').data, u'19/42') + self.assert_equal(c.get('/1/bar').data, u'23') + self.assert_equal(c.get('/2/bar').data, u'19') + + def test_blueprint_url_processors(self): + bp = flask.Blueprint('frontend', __name__, url_prefix='/') + + @bp.url_defaults + def add_language_code(endpoint, values): + values.setdefault('lang_code', flask.g.lang_code) + + @bp.url_value_preprocessor + def pull_lang_code(endpoint, values): + flask.g.lang_code = values.pop('lang_code') + + @bp.route('/') + def index(): + return flask.url_for('.about') + + @bp.route('/about') + def about(): + return flask.url_for('.index') + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + + self.assert_equal(c.get('/de/').data, '/de/about') + self.assert_equal(c.get('/de/about').data, '/de/') + + def test_templates_and_static(self): + from blueprintapp import app + c = app.test_client() + + rv = c.get('/') + assert rv.data == 'Hello from the Frontend' + rv = c.get('/admin/') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/index2') + assert rv.data == 'Hello from the Admin' + rv = c.get('/admin/static/test.txt') + assert rv.data.strip() == 'Admin File' + rv = c.get('/admin/static/css/test.css') + assert rv.data.strip() == '/* nested file */' + + with app.test_request_context(): + assert flask.url_for('admin.static', filename='test.txt') \ + == '/admin/static/test.txt' + + with app.test_request_context(): + try: + flask.render_template('missing.html') + except TemplateNotFound, e: + assert e.name == 'missing.html' + else: + assert 0, 'expected exception' + + with flask.Flask(__name__).test_request_context(): + assert flask.render_template('nested/nested.txt') == 'I\'m nested' + + def test_templates_list(self): + from blueprintapp import app + templates = sorted(app.jinja_env.list_templates()) + self.assert_equal(templates, ['admin/index.html', + 'frontend/index.html']) + + def test_dotted_names(self): + frontend = flask.Blueprint('myapp.frontend', __name__) + backend = flask.Blueprint('myapp.backend', __name__) + + @frontend.route('/fe') + def frontend_index(): + return flask.url_for('myapp.backend.backend_index') + + @frontend.route('/fe2') + def frontend_page2(): + return flask.url_for('.frontend_index') + + @backend.route('/be') + def backend_index(): + return flask.url_for('myapp.frontend.frontend_index') + + app = flask.Flask(__name__) + app.register_blueprint(frontend) + app.register_blueprint(backend) + + c = app.test_client() + self.assert_equal(c.get('/fe').data.strip(), '/be') + self.assert_equal(c.get('/fe2').data.strip(), '/fe') + self.assert_equal(c.get('/be').data.strip(), '/fe') + + def test_empty_url_defaults(self): + bp = flask.Blueprint('bp', __name__) + + @bp.route('/', defaults={'page': 1}) + @bp.route('/page/') + def something(page): + return str(page) + + app = flask.Flask(__name__) + app.register_blueprint(bp) + + c = app.test_client() + self.assert_equal(c.get('/').data, '1') + self.assert_equal(c.get('/page/2').data, '2') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BlueprintTestCase)) + suite.addTest(unittest.makeSuite(ModuleTestCase)) + return suite diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py new file mode 100644 index 00000000..c8bf4687 --- /dev/null +++ b/flask/testsuite/config.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.config + ~~~~~~~~~~~~~~~~~~~~~~ + + Configuration and instances. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import sys +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +# config keys used for the ConfigTestCase +TEST_KEY = 'foo' +SECRET_KEY = 'devkey' + + +class ConfigTestCase(FlaskTestCase): + + def common_object_test(self, app): + assert app.secret_key == 'devkey' + assert app.config['TEST_KEY'] == 'foo' + assert 'ConfigTestCase' not in app.config + + def test_config_from_file(self): + app = flask.Flask(__name__) + app.config.from_pyfile(__file__.rsplit('.')[0] + '.py') + self.common_object_test(app) + + def test_config_from_object(self): + app = flask.Flask(__name__) + app.config.from_object(__name__) + self.common_object_test(app) + + def test_config_from_class(self): + class Base(object): + TEST_KEY = 'foo' + class Test(Base): + SECRET_KEY = 'devkey' + app = flask.Flask(__name__) + app.config.from_object(Test) + self.common_object_test(app) + + def test_config_from_envvar(self): + env = os.environ + try: + os.environ = {} + app = flask.Flask(__name__) + try: + app.config.from_envvar('FOO_SETTINGS') + except RuntimeError, e: + assert "'FOO_SETTINGS' is not set" in str(e) + else: + assert 0, 'expected exception' + assert not app.config.from_envvar('FOO_SETTINGS', silent=True) + + os.environ = {'FOO_SETTINGS': __file__.rsplit('.')[0] + '.py'} + assert app.config.from_envvar('FOO_SETTINGS') + self.common_object_test(app) + finally: + os.environ = env + + def test_config_missing(self): + app = flask.Flask(__name__) + try: + app.config.from_pyfile('missing.cfg') + except IOError, e: + msg = str(e) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.cfg'") + else: + assert 0, 'expected config' + assert not app.config.from_pyfile('missing.cfg', silent=True) + + +class InstanceTestCase(FlaskTestCase): + + def test_explicit_instance_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + try: + flask.Flask(__name__, instance_path='instance') + except ValueError, e: + self.assert_('must be absolute' in str(e)) + else: + self.fail('Expected value error') + + app = flask.Flask(__name__, instance_path=here) + self.assert_equal(app.instance_path, here) + + def test_uninstalled_module_paths(self): + from config_module_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_uninstalled_package_paths(self): + from config_package_app import app + here = os.path.abspath(os.path.dirname(__file__)) + self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) + + def test_installed_module_paths(self): + import types + expected_prefix = os.path.abspath('foo') + mod = types.ModuleType('myapp') + mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_installed_package_paths(self): + import types + expected_prefix = os.path.abspath('foo') + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_prefix_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + def test_egg_installed_paths(self): + import types + expected_prefix = os.path.abspath(sys.prefix) + package_path = os.path.join(expected_prefix, 'lib', 'python2.5', + 'site-packages', 'MyApp.egg', 'myapp') + mod = types.ModuleType('myapp') + mod.__path__ = [package_path] + mod.__file__ = os.path.join(package_path, '__init__.py') + sys.modules['myapp'] = mod + try: + mod.app = flask.Flask(mod.__name__) + self.assert_equal(mod.app.instance_path, + os.path.join(expected_prefix, 'var', + 'myapp-instance')) + finally: + sys.modules['myapp'] = None + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ConfigTestCase)) + suite.addTest(unittest.makeSuite(InstanceTestCase)) + return suite diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py new file mode 100644 index 00000000..d691b1dd --- /dev/null +++ b/flask/testsuite/deprecations.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.deprecations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests deprecation support. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase, catch_warnings + + +class DeprecationsTestCase(FlaskTestCase): + + def test_init_jinja_globals(self): + class MyFlask(flask.Flask): + def init_jinja_globals(self): + self.jinja_env.globals['foo'] = '42' + + with catch_warnings() as log: + app = MyFlask(__name__) + @app.route('/') + def foo(): + return app.jinja_env.globals['foo'] + + c = app.test_client() + assert c.get('/').data == '42' + assert len(log) == 1 + assert 'init_jinja_globals' in str(log[0]['message']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(DeprecationsTestCase)) + return suite diff --git a/flask/testsuite/examples.py b/flask/testsuite/examples.py new file mode 100644 index 00000000..2d30958f --- /dev/null +++ b/flask/testsuite/examples.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.examples + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests the examples. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import unittest +from flask.testsuite import add_to_path + + +def setup_path(): + example_path = os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir, 'examples') + add_to_path(os.path.join(example_path, 'flaskr')) + add_to_path(os.path.join(example_path, 'minitwit')) + + +def suite(): + setup_path() + suite = unittest.TestSuite() + try: + from minitwit_tests import MiniTwitTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(MiniTwitTestCase)) + try: + from flaskr_tests import FlaskrTestCase + except ImportError: + pass + else: + suite.addTest(unittest.makeSuite(FlaskrTestCase)) + return suite diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py new file mode 100644 index 00000000..faea9c8d --- /dev/null +++ b/flask/testsuite/helpers.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.helpers + ~~~~~~~~~~~~~~~~~~~~~~~ + + Various helpers. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os +import flask +import unittest +from logging import StreamHandler +from StringIO import StringIO +from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr +from werkzeug.http import parse_options_header + + +def has_encoding(name): + try: + import codecs + codecs.lookup(name) + return True + except LookupError: + return False + + +class JSONTestCase(FlaskTestCase): + + def test_json_bad_requests(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + + def test_json_body_encoding(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.json + + c = app.test_client() + resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') + assert resp.data == u'Hällo Wörld'.encode('utf-8') + + def test_jsonify(self): + d = dict(a=23, b=42, c=[1, 2, 3]) + app = flask.Flask(__name__) + @app.route('/kw') + def return_kwargs(): + return flask.jsonify(**d) + @app.route('/dict') + def return_dict(): + return flask.jsonify(d) + c = app.test_client() + for url in '/kw', '/dict': + rv = c.get(url) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data) == d + + def test_json_attr(self): + app = flask.Flask(__name__) + @app.route('/add', methods=['POST']) + def add(): + return unicode(flask.request.json['a'] + flask.request.json['b']) + c = app.test_client() + rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') + assert rv.data == '3' + + def test_template_escaping(self): + app = flask.Flask(__name__) + render = flask.render_template_string + with app.test_request_context(): + rv = render('{{ ""|tojson|safe }}') + assert rv == '"<\\/script>"' + rv = render('{{ "<\0/script>"|tojson|safe }}') + assert rv == '"<\\u0000\\/script>"' + + def test_modified_url_encoding(self): + class ModifiedRequest(flask.Request): + url_charset = 'euc-kr' + app = flask.Flask(__name__) + app.request_class = ModifiedRequest + app.url_map.charset = 'euc-kr' + + @app.route('/') + def index(): + return flask.request.args['foo'] + + rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) + assert rv.status_code == 200 + assert rv.data == u'정상처리'.encode('utf-8') + + if not has_encoding('euc-kr'): + test_modified_url_encoding = None + + +class SendfileTestCase(FlaskTestCase): + + def test_send_file_regular(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.send_file('static/index.html') + assert rv.direct_passthrough + assert rv.mimetype == 'text/html' + with app.open_resource('static/index.html') as f: + assert rv.data == f.read() + + def test_send_file_xsendfile(self): + app = flask.Flask(__name__) + app.use_x_sendfile = True + with app.test_request_context(): + rv = flask.send_file('static/index.html') + assert rv.direct_passthrough + assert 'x-sendfile' in rv.headers + assert rv.headers['x-sendfile'] == \ + os.path.join(app.root_path, 'static/index.html') + assert rv.mimetype == 'text/html' + + def test_send_file_object(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + with app.open_resource('static/index.html') as f: + assert rv.data == f.read() + assert rv.mimetype == 'text/html' + # mimetypes + etag + assert len(captured) == 2 + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f) + assert rv.mimetype == 'text/html' + assert 'x-sendfile' in rv.headers + assert rv.headers['x-sendfile'] == \ + os.path.join(app.root_path, 'static/index.html') + # mimetypes + etag + assert len(captured) == 2 + + app.use_x_sendfile = False + with app.test_request_context(): + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f) + assert rv.data == 'Test' + assert rv.mimetype == 'application/octet-stream' + # etags + assert len(captured) == 1 + with catch_warnings() as captured: + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + assert rv.data == 'Test' + assert rv.mimetype == 'text/plain' + # etags + assert len(captured) == 1 + + app.use_x_sendfile = True + with catch_warnings() as captured: + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f) + assert 'x-sendfile' not in rv.headers + # etags + assert len(captured) == 1 + + def test_attachment(self): + app = flask.Flask(__name__) + with catch_warnings() as captured: + with app.test_request_context(): + f = open(os.path.join(app.root_path, 'static/index.html')) + rv = flask.send_file(f, as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + # mimetypes + etag + assert len(captured) == 2 + + with app.test_request_context(): + assert options['filename'] == 'index.html' + rv = flask.send_file('static/index.html', as_attachment=True) + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + assert options['filename'] == 'index.html' + + with app.test_request_context(): + rv = flask.send_file(StringIO('Test'), as_attachment=True, + attachment_filename='index.txt', + add_etags=False) + assert rv.mimetype == 'text/plain' + value, options = parse_options_header(rv.headers['Content-Disposition']) + assert value == 'attachment' + assert options['filename'] == 'index.txt' + + +class LoggingTestCase(FlaskTestCase): + + def test_logger_cache(self): + app = flask.Flask(__name__) + logger1 = app.logger + assert app.logger is logger1 + assert logger1.name == __name__ + app.logger_name = __name__ + '/test_logger_cache' + assert app.logger is not logger1 + + def test_debug_log(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/') + def index(): + app.logger.warning('the standard library is dead') + app.logger.debug('this is a debug statement') + return '' + + @app.route('/exc') + def exc(): + 1/0 + + with app.test_client() as c: + with catch_stderr() as err: + c.get('/') + out = err.getvalue() + assert 'WARNING in helpers [' in out + assert os.path.basename(__file__.rsplit('.')[0] + '.py') in out + assert 'the standard library is dead' in out + assert 'this is a debug statement' in out + + with catch_stderr() as err: + try: + c.get('/exc') + except ZeroDivisionError: + pass + else: + assert False, 'debug log ate the exception' + + def test_exception_logging(self): + out = StringIO() + app = flask.Flask(__name__) + app.logger_name = 'flask_tests/test_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert 'Internal Server Error' in rv.data + + err = out.getvalue() + assert 'Exception on / [GET]' in err + assert 'Traceback (most recent call last):' in err + assert '1/0' in err + assert 'ZeroDivisionError:' in err + + def test_processor_exceptions(self): + app = flask.Flask(__name__) + @app.before_request + def before_request(): + if trigger == 'before': + 1/0 + @app.after_request + def after_request(response): + if trigger == 'after': + 1/0 + return response + @app.route('/') + def index(): + return 'Foo' + @app.errorhandler(500) + def internal_server_error(e): + return 'Hello Server Error', 500 + for trigger in 'before', 'after': + rv = app.test_client().get('/') + assert rv.status_code == 500 + assert rv.data == 'Hello Server Error' + + +def suite(): + suite = unittest.TestSuite() + if flask.json_available: + suite.addTest(unittest.makeSuite(JSONTestCase)) + suite.addTest(unittest.makeSuite(SendfileTestCase)) + suite.addTest(unittest.makeSuite(LoggingTestCase)) + return suite diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py new file mode 100644 index 00000000..e55807b3 --- /dev/null +++ b/flask/testsuite/signals.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.signals + ~~~~~~~~~~~~~~~~~~~~~~~ + + Signalling. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class SignalsTestCase(FlaskTestCase): + + def test_template_rendered(self): + app = flask.Flask(__name__) + + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + recorded = [] + def record(sender, template, context): + recorded.append((template, context)) + + flask.template_rendered.connect(record, app) + try: + app.test_client().get('/') + assert len(recorded) == 1 + template, context = recorded[0] + assert template.name == 'simple_template.html' + assert context['whiskey'] == 42 + finally: + flask.template_rendered.disconnect(record, app) + + def test_request_signals(self): + app = flask.Flask(__name__) + calls = [] + + def before_request_signal(sender): + calls.append('before-signal') + + def after_request_signal(sender, response): + assert response.data == 'stuff' + calls.append('after-signal') + + @app.before_request + def before_request_handler(): + calls.append('before-handler') + + @app.after_request + def after_request_handler(response): + calls.append('after-handler') + response.data = 'stuff' + return response + + @app.route('/') + def index(): + calls.append('handler') + return 'ignored anyway' + + flask.request_started.connect(before_request_signal, app) + flask.request_finished.connect(after_request_signal, app) + + try: + rv = app.test_client().get('/') + assert rv.data == 'stuff' + + assert calls == ['before-signal', 'before-handler', + 'handler', 'after-handler', + 'after-signal'] + finally: + flask.request_started.disconnect(before_request_signal, app) + flask.request_finished.disconnect(after_request_signal, app) + + def test_request_exception_signal(self): + app = flask.Flask(__name__) + recorded = [] + + @app.route('/') + def index(): + 1/0 + + def record(sender, exception): + recorded.append(exception) + + flask.got_request_exception.connect(record, app) + try: + assert app.test_client().get('/').status_code == 500 + assert len(recorded) == 1 + assert isinstance(recorded[0], ZeroDivisionError) + finally: + flask.got_request_exception.disconnect(record, app) + + +def suite(): + suite = unittest.TestSuite() + if flask.signals_available: + suite.addTest(unittest.makeSuite(SignalsTestCase)) + return suite diff --git a/tests/static/index.html b/flask/testsuite/static/index.html similarity index 100% rename from tests/static/index.html rename to flask/testsuite/static/index.html diff --git a/tests/templates/_macro.html b/flask/testsuite/templates/_macro.html similarity index 100% rename from tests/templates/_macro.html rename to flask/testsuite/templates/_macro.html diff --git a/tests/templates/context_template.html b/flask/testsuite/templates/context_template.html similarity index 100% rename from tests/templates/context_template.html rename to flask/testsuite/templates/context_template.html diff --git a/tests/templates/escaping_template.html b/flask/testsuite/templates/escaping_template.html similarity index 100% rename from tests/templates/escaping_template.html rename to flask/testsuite/templates/escaping_template.html diff --git a/tests/templates/mail.txt b/flask/testsuite/templates/mail.txt similarity index 100% rename from tests/templates/mail.txt rename to flask/testsuite/templates/mail.txt diff --git a/tests/templates/nested/nested.txt b/flask/testsuite/templates/nested/nested.txt similarity index 100% rename from tests/templates/nested/nested.txt rename to flask/testsuite/templates/nested/nested.txt diff --git a/tests/templates/simple_template.html b/flask/testsuite/templates/simple_template.html similarity index 100% rename from tests/templates/simple_template.html rename to flask/testsuite/templates/simple_template.html diff --git a/tests/templates/template_filter.html b/flask/testsuite/templates/template_filter.html similarity index 100% rename from tests/templates/template_filter.html rename to flask/testsuite/templates/template_filter.html diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py new file mode 100644 index 00000000..e980ff92 --- /dev/null +++ b/flask/testsuite/templating.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.templating + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Template functionality + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TemplatingTestCase(FlaskTestCase): + + def test_context_processing(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'injected_value': 42} + @app.route('/') + def index(): + return flask.render_template('context_template.html', value=23) + rv = app.test_client().get('/') + assert rv.data == '

    23|42' + + def test_original_win(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template_string('{{ config }}', config=42) + rv = app.test_client().get('/') + assert rv.data == '42' + + def test_standard_context(self): + app = flask.Flask(__name__) + app.secret_key = 'development key' + @app.route('/') + def index(): + flask.g.foo = 23 + flask.session['test'] = 'aha' + return flask.render_template_string(''' + {{ request.args.foo }} + {{ g.foo }} + {{ config.DEBUG }} + {{ session.test }} + ''') + rv = app.test_client().get('/?foo=42') + assert rv.data.split() == ['42', '23', 'False', 'aha'] + + def test_escaping(self): + text = '

    Hello World!' + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('escaping_template.html', text=text, + html=flask.Markup(text)) + lines = app.test_client().get('/').data.splitlines() + assert lines == [ + '<p>Hello World!', + '

    Hello World!', + '

    Hello World!', + '

    Hello World!', + '<p>Hello World!', + '

    Hello World!' + ] + + def test_no_escaping(self): + app = flask.Flask(__name__) + with app.test_request_context(): + assert flask.render_template_string('{{ foo }}', + foo='') == '' + assert flask.render_template('mail.txt', foo='') \ + == ' Mail' + + def test_macros(self): + app = flask.Flask(__name__) + with app.test_request_context(): + macro = flask.get_template_attribute('_macro.html', 'hello') + assert macro('World') == 'Hello World!' + + def test_template_filter(self): + app = flask.Flask(__name__) + @app.template_filter() + def my_reverse(s): + return s[::-1] + assert 'my_reverse' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['my_reverse'] == my_reverse + assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + + def test_template_filter_with_name(self): + app = flask.Flask(__name__) + @app.template_filter('strrev') + def my_reverse(s): + return s[::-1] + assert 'strrev' in app.jinja_env.filters.keys() + assert app.jinja_env.filters['strrev'] == my_reverse + assert app.jinja_env.filters['strrev']('abcd') == 'dcba' + + def test_template_filter_with_template(self): + app = flask.Flask(__name__) + @app.template_filter() + def super_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def test_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + assert rv.data == 'dcba' + + def test_custom_template_loader(self): + class MyFlask(flask.Flask): + def create_global_jinja_loader(self): + from jinja2 import DictLoader + return DictLoader({'index.html': 'Hello Custom World!'}) + app = MyFlask(__name__) + @app.route('/') + def index(): + return flask.render_template('index.html') + c = app.test_client() + rv = c.get('/') + assert rv.data == 'Hello Custom World!' + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TemplatingTestCase)) + return suite diff --git a/tests/blueprintapp/__init__.py b/flask/testsuite/test_apps/blueprintapp/__init__.py similarity index 100% rename from tests/blueprintapp/__init__.py rename to flask/testsuite/test_apps/blueprintapp/__init__.py diff --git a/tests/blueprintapp/apps/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/__init__.py similarity index 100% rename from tests/blueprintapp/apps/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/__init__.py diff --git a/tests/blueprintapp/apps/admin/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py similarity index 100% rename from tests/blueprintapp/apps/admin/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/admin/__init__.py diff --git a/tests/blueprintapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css similarity index 100% rename from tests/blueprintapp/apps/admin/static/css/test.css rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/css/test.css diff --git a/tests/blueprintapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt similarity index 100% rename from tests/blueprintapp/apps/admin/static/test.txt rename to flask/testsuite/test_apps/blueprintapp/apps/admin/static/test.txt diff --git a/tests/blueprintapp/apps/admin/templates/admin/index.html b/flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html similarity index 100% rename from tests/blueprintapp/apps/admin/templates/admin/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/admin/templates/admin/index.html diff --git a/tests/blueprintapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py similarity index 100% rename from tests/blueprintapp/apps/frontend/__init__.py rename to flask/testsuite/test_apps/blueprintapp/apps/frontend/__init__.py diff --git a/tests/blueprintapp/apps/frontend/templates/frontend/index.html b/flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html similarity index 100% rename from tests/blueprintapp/apps/frontend/templates/frontend/index.html rename to flask/testsuite/test_apps/blueprintapp/apps/frontend/templates/frontend/index.html diff --git a/flask/testsuite/test_apps/config_module_app.py b/flask/testsuite/test_apps/config_module_app.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_module_app.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/config_package_app/__init__.py b/flask/testsuite/test_apps/config_package_app/__init__.py new file mode 100644 index 00000000..380d46bf --- /dev/null +++ b/flask/testsuite/test_apps/config_package_app/__init__.py @@ -0,0 +1,4 @@ +import os +import flask +here = os.path.abspath(os.path.dirname(__file__)) +app = flask.Flask(__name__) diff --git a/tests/moduleapp/__init__.py b/flask/testsuite/test_apps/moduleapp/__init__.py similarity index 100% rename from tests/moduleapp/__init__.py rename to flask/testsuite/test_apps/moduleapp/__init__.py diff --git a/tests/moduleapp/apps/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/__init__.py similarity index 100% rename from tests/moduleapp/apps/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/__init__.py diff --git a/tests/moduleapp/apps/admin/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py similarity index 100% rename from tests/moduleapp/apps/admin/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/admin/__init__.py diff --git a/tests/moduleapp/apps/admin/static/css/test.css b/flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css similarity index 100% rename from tests/moduleapp/apps/admin/static/css/test.css rename to flask/testsuite/test_apps/moduleapp/apps/admin/static/css/test.css diff --git a/tests/moduleapp/apps/admin/static/test.txt b/flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt similarity index 100% rename from tests/moduleapp/apps/admin/static/test.txt rename to flask/testsuite/test_apps/moduleapp/apps/admin/static/test.txt diff --git a/tests/moduleapp/apps/admin/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html similarity index 100% rename from tests/moduleapp/apps/admin/templates/index.html rename to flask/testsuite/test_apps/moduleapp/apps/admin/templates/index.html diff --git a/tests/moduleapp/apps/frontend/__init__.py b/flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py similarity index 100% rename from tests/moduleapp/apps/frontend/__init__.py rename to flask/testsuite/test_apps/moduleapp/apps/frontend/__init__.py diff --git a/tests/moduleapp/apps/frontend/templates/index.html b/flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html similarity index 100% rename from tests/moduleapp/apps/frontend/templates/index.html rename to flask/testsuite/test_apps/moduleapp/apps/frontend/templates/index.html diff --git a/tests/subdomaintestmodule/__init__.py b/flask/testsuite/test_apps/subdomaintestmodule/__init__.py similarity index 100% rename from tests/subdomaintestmodule/__init__.py rename to flask/testsuite/test_apps/subdomaintestmodule/__init__.py diff --git a/tests/subdomaintestmodule/static/hello.txt b/flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt similarity index 100% rename from tests/subdomaintestmodule/static/hello.txt rename to flask/testsuite/test_apps/subdomaintestmodule/static/hello.txt diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py new file mode 100644 index 00000000..4e16c257 --- /dev/null +++ b/flask/testsuite/testing.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.testing + ~~~~~~~~~~~~~~~~~~~~~~~ + + Test client and more. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class TestToolsTestCase(FlaskTestCase): + + def test_environ_defaults_from_config(self): + app = flask.Flask(__name__) + app.testing = True + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://example.com:1234/foo/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://example.com:1234/foo/') + + def test_environ_defaults(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context() + self.assert_equal(ctx.request.url, 'http://localhost/') + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.data, 'http://localhost/') + + def test_session_transactions(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + @app.route('/') + def index(): + return unicode(flask.session['foo']) + + with app.test_client() as c: + with c.session_transaction() as sess: + self.assert_equal(len(sess), 0) + sess['foo'] = [42] + self.assert_equal(len(sess), 1) + rv = c.get('/') + self.assert_equal(rv.data, '[42]') + + def test_session_transactions_no_null_sessions(self): + app = flask.Flask(__name__) + app.testing = True + + with app.test_client() as c: + try: + with c.session_transaction() as sess: + pass + except RuntimeError, e: + self.assert_('Session backend did not open a session' in str(e)) + else: + self.fail('Expected runtime error') + + def test_session_transactions_keep_context(self): + app = flask.Flask(__name__) + app.testing = True + app.secret_key = 'testing' + + with app.test_client() as c: + rv = c.get('/') + req = flask.request._get_current_object() + with c.session_transaction(): + self.assert_(req is flask.request._get_current_object()) + + def test_test_client_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + flask.g.value = 42 + return 'Hello World!' + + @app.route('/other') + def other(): + 1/0 + + with app.test_client() as c: + resp = c.get('/') + assert flask.g.value == 42 + assert resp.data == 'Hello World!' + assert resp.status_code == 200 + + resp = c.get('/other') + assert not hasattr(flask.g, 'value') + assert 'Internal Server Error' in resp.data + assert resp.status_code == 500 + flask.g.value = 23 + + try: + flask.g.value + except (AttributeError, RuntimeError): + pass + else: + raise AssertionError('some kind of exception expected') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TestToolsTestCase)) + return suite diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py new file mode 100644 index 00000000..a89c44ac --- /dev/null +++ b/flask/testsuite/views.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.views + ~~~~~~~~~~~~~~~~~~~~~ + + Pluggable views. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import flask.views +import unittest +from flask.testsuite import FlaskTestCase +from werkzeug.http import parse_set_header + + +class ViewTestCase(FlaskTestCase): + + def common_test(self, app): + c = app.test_client() + + self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.post('/').data, 'POST') + self.assert_equal(c.put('/').status_code, 405) + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_basic_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + self.common_test(app) + + def test_method_based_view(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + app.add_url_rule('/', view_func=Index.as_view('index')) + + self.common_test(app) + + def test_view_patching(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + 1/0 + def post(self): + 1/0 + + class Other(Index): + def get(self): + return 'GET' + def post(self): + return 'POST' + + view = Index.as_view('index') + view.view_class = Other + app.add_url_rule('/', view_func=view) + self.common_test(app) + + def test_view_inheritance(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def post(self): + return 'POST' + + class BetterIndex(Index): + def delete(self): + return 'DELETE' + + app.add_url_rule('/', view_func=BetterIndex.as_view('index')) + c = app.test_client() + + meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) + self.assert_equal(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) + + def test_view_decorators(self): + app = flask.Flask(__name__) + + def add_x_parachute(f): + def new_function(*args, **kwargs): + resp = flask.make_response(f(*args, **kwargs)) + resp.headers['X-Parachute'] = 'awesome' + return resp + return new_function + + class Index(flask.views.View): + decorators = [add_x_parachute] + def dispatch_request(self): + return 'Awesome' + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.headers['X-Parachute'], 'awesome') + self.assert_equal(rv.data, 'Awesome') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ViewTestCase)) + return suite diff --git a/tests/flaskext_test.py b/scripts/flaskext_test.py similarity index 100% rename from tests/flaskext_test.py rename to scripts/flaskext_test.py diff --git a/setup.py b/setup.py index 1809e05e..a51c3887 100644 --- a/setup.py +++ b/setup.py @@ -77,12 +77,6 @@ class run_audit(Command): else: print ("No problems found in sourcecode.") -def run_tests(): - import os, sys - sys.path.append(os.path.join(os.path.dirname(__file__), 'tests')) - from flask_tests import suite - return suite() - setup( name='Flask', @@ -112,5 +106,5 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules' ], cmdclass={'audit': run_audit}, - test_suite='__main__.run_tests' + test_suite='flask.testsuite.suite' ) diff --git a/tests/flask_tests.py b/tests/flask_tests.py deleted file mode 100644 index dcc3e1a0..00000000 --- a/tests/flask_tests.py +++ /dev/null @@ -1,2329 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Flask Tests - ~~~~~~~~~~~ - - Tests Flask itself. The majority of Flask is already tested - as part of Werkzeug. - - :copyright: (c) 2010 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from __future__ import with_statement -import os -import re -import sys -import flask -import flask.views -import unittest -import warnings -from threading import Thread -from logging import StreamHandler -from contextlib import contextmanager -from functools import update_wrapper -from datetime import datetime -from werkzeug import parse_date, parse_options_header -from werkzeug.exceptions import NotFound, BadRequest -from werkzeug.http import parse_set_header -from jinja2 import TemplateNotFound -from cStringIO import StringIO - -example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') -sys.path.append(os.path.join(example_path, 'flaskr')) -sys.path.append(os.path.join(example_path, 'minitwit')) - - -def has_encoding(name): - try: - import codecs - codecs.lookup(name) - return True - except LookupError: - return False - - -# config keys used for the ConfigTestCase -TEST_KEY = 'foo' -SECRET_KEY = 'devkey' - - -# import moduleapp here because it uses deprecated features and we don't -# want to see the warnings -warnings.simplefilter('ignore', DeprecationWarning) -from moduleapp import app as moduleapp -warnings.simplefilter('default', DeprecationWarning) - - -@contextmanager -def catch_warnings(): - """Catch warnings in a with block in a list""" - # make sure deprecation warnings are active in tests - warnings.simplefilter('default', category=DeprecationWarning) - - filters = warnings.filters - warnings.filters = filters[:] - old_showwarning = warnings.showwarning - log = [] - def showwarning(message, category, filename, lineno, file=None, line=None): - log.append(locals()) - try: - warnings.showwarning = showwarning - yield log - finally: - warnings.filters = filters - warnings.showwarning = old_showwarning - - -@contextmanager -def catch_stderr(): - """Catch stderr in a StringIO""" - old_stderr = sys.stderr - sys.stderr = rv = StringIO() - try: - yield rv - finally: - sys.stderr = old_stderr - - -def emits_module_deprecation_warning(f): - def new_f(*args, **kwargs): - with catch_warnings() as log: - f(*args, **kwargs) - assert log, 'expected deprecation warning' - for entry in log: - assert 'Modules are deprecated' in str(entry['message']) - return update_wrapper(new_f, f) - - -class FlaskTestCase(unittest.TestCase): - - def ensure_clean_request_context(self): - # make sure we're not leaking a request context since we are - # testing flask internally in debug mode in a few cases - self.assertEqual(flask._request_ctx_stack.top, None) - - def tearDown(self): - unittest.TestCase.tearDown(self) - self.ensure_clean_request_context() - - -class ContextTestCase(FlaskTestCase): - - def test_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - @app.route('/meh') - def meh(): - return flask.request.url - - with app.test_request_context('/?name=World'): - assert index() == 'Hello World!' - with app.test_request_context('/meh'): - assert meh() == 'http://localhost/meh' - assert flask._request_ctx_stack.top is None - - def test_context_test(self): - app = flask.Flask(__name__) - assert not flask.request - assert not flask.has_request_context() - ctx = app.test_request_context() - ctx.push() - try: - assert flask.request - assert flask.has_request_context() - finally: - ctx.pop() - - def test_manual_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - - ctx = app.test_request_context('/?name=World') - ctx.push() - assert index() == 'Hello World!' - ctx.pop() - try: - index() - except RuntimeError: - pass - else: - assert 0, 'expected runtime error' - - -class BasicFunctionalityTestCase(FlaskTestCase): - - def test_options_work(self): - app = flask.Flask(__name__) - @app.route('/', methods=['GET', 'POST']) - def index(): - return 'Hello World' - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - assert rv.data == '' - - def test_options_on_multiple_rules(self): - app = flask.Flask(__name__) - @app.route('/', methods=['GET', 'POST']) - def index(): - return 'Hello World' - @app.route('/', methods=['PUT']) - def index_put(): - return 'Aha!' - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] - - def test_options_handling_disabled(self): - app = flask.Flask(__name__) - def index(): - return 'Hello World!' - index.provide_automatic_options = False - app.route('/')(index) - rv = app.test_client().open('/', method='OPTIONS') - assert rv.status_code == 405 - - app = flask.Flask(__name__) - def index2(): - return 'Hello World!' - index2.provide_automatic_options = True - app.route('/', methods=['OPTIONS'])(index2) - rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['OPTIONS'] - - def test_request_dispatching(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.request.method - @app.route('/more', methods=['GET', 'POST']) - def more(): - return flask.request.method - - c = app.test_client() - assert c.get('/').data == 'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - - def test_url_mapping(self): - app = flask.Flask(__name__) - def index(): - return flask.request.method - def more(): - return flask.request.method - - app.add_url_rule('/', 'index', index) - app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) - - c = app.test_client() - assert c.get('/').data == 'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - - def test_werkzeug_routing(self): - from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - def bar(): - return 'bar' - def index(): - return 'index' - app.view_functions['bar'] = bar - app.view_functions['index'] = index - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - def test_endpoint_decorator(self): - from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - - @app.endpoint('bar') - def bar(): - return 'bar' - - @app.endpoint('index') - def index(): - return 'index' - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - def test_session(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - @app.route('/set', methods=['POST']) - def set(): - flask.session['value'] = flask.request.form['value'] - return 'value set' - @app.route('/get') - def get(): - return flask.session['value'] - - c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == 'value set' - assert c.get('/get').data == '42' - - def test_session_using_server_name(self): - app = flask.Flask(__name__) - app.config.update( - SECRET_KEY='foo', - SERVER_NAME='example.com' - ) - @app.route('/') - def index(): - flask.session['testing'] = 42 - return 'Hello World' - rv = app.test_client().get('/', 'http://example.com/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() - - def test_session_using_server_name_and_port(self): - app = flask.Flask(__name__) - app.config.update( - SECRET_KEY='foo', - SERVER_NAME='example.com:8080' - ) - @app.route('/') - def index(): - flask.session['testing'] = 42 - return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() - - def test_session_using_application_root(self): - class PrefixPathMiddleware(object): - def __init__(self, app, prefix): - self.app = app - self.prefix = prefix - def __call__(self, environ, start_response): - environ['SCRIPT_NAME'] = self.prefix - return self.app(environ, start_response) - - app = flask.Flask(__name__) - app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar') - app.config.update( - SECRET_KEY='foo', - APPLICATION_ROOT='/bar' - ) - @app.route('/') - def index(): - flask.session['testing'] = 42 - return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') - assert 'path=/bar' in rv.headers['set-cookie'].lower() - - def test_missing_session(self): - app = flask.Flask(__name__) - def expect_exception(f, *args, **kwargs): - try: - f(*args, **kwargs) - except RuntimeError, e: - assert e.args and 'session is unavailable' in e.args[0] - else: - assert False, 'expected exception' - with app.test_request_context(): - assert flask.session.get('missing_key') is None - expect_exception(flask.session.__setitem__, 'foo', 42) - expect_exception(flask.session.pop, 'foo') - - def test_session_expiration(self): - permanent = True - app = flask.Flask(__name__) - app.secret_key = 'testkey' - @app.route('/') - def index(): - flask.session['test'] = 42 - flask.session.permanent = permanent - return '' - - @app.route('/test') - def test(): - return unicode(flask.session.permanent) - - client = app.test_client() - rv = client.get('/') - assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - expires = parse_date(match.group()) - expected = datetime.utcnow() + app.permanent_session_lifetime - assert expires.year == expected.year - assert expires.month == expected.month - assert expires.day == expected.day - - rv = client.get('/test') - assert rv.data == 'True' - - permanent = False - rv = app.test_client().get('/') - assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - assert match is None - - def test_flashes(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - - with app.test_request_context(): - assert not flask.session.modified - flask.flash('Zap') - flask.session.modified = False - flask.flash('Zip') - assert flask.session.modified - assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] - - def test_extended_flashing(self): - app = flask.Flask(__name__) - app.secret_key = 'testkey' - - @app.route('/') - def index(): - flask.flash(u'Hello World') - flask.flash(u'Hello World', 'error') - flask.flash(flask.Markup(u'Testing'), 'warning') - return '' - - @app.route('/test') - def test(): - messages = flask.get_flashed_messages(with_categories=True) - assert len(messages) == 3 - assert messages[0] == ('message', u'Hello World') - assert messages[1] == ('error', u'Hello World') - assert messages[2] == ('warning', flask.Markup(u'Testing')) - return '' - messages = flask.get_flashed_messages() - assert len(messages) == 3 - assert messages[0] == u'Hello World' - assert messages[1] == u'Hello World' - assert messages[2] == flask.Markup(u'Testing') - - c = app.test_client() - c.get('/') - c.get('/test') - - def test_request_processing(self): - app = flask.Flask(__name__) - evts = [] - @app.before_request - def before_request(): - evts.append('before') - @app.after_request - def after_request(response): - response.data += '|after' - evts.append('after') - return response - @app.route('/') - def index(): - assert 'before' in evts - assert 'after' not in evts - return 'request' - assert 'after' not in evts - rv = app.test_client().get('/').data - assert 'after' in evts - assert rv == 'request|after' - - def test_teardown_request_handler(self): - called = [] - app = flask.Flask(__name__) - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - @app.route('/') - def root(): - return "Response" - rv = app.test_client().get('/') - assert rv.status_code == 200 - assert 'Response' in rv.data - assert len(called) == 1 - - def test_teardown_request_handler_debug_mode(self): - called = [] - app = flask.Flask(__name__) - app.testing = True - @app.teardown_request - def teardown_request(exc): - called.append(True) - return "Ignored" - @app.route('/') - def root(): - return "Response" - rv = app.test_client().get('/') - assert rv.status_code == 200 - assert 'Response' in rv.data - assert len(called) == 1 - - def test_teardown_request_handler_error(self): - called = [] - app = flask.Flask(__name__) - @app.teardown_request - def teardown_request1(exc): - assert type(exc) == ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError - except: - pass - @app.teardown_request - def teardown_request2(exc): - assert type(exc) == ZeroDivisionError - called.append(True) - # This raises a new error and blows away sys.exc_info(), so we can - # test that all teardown_requests get passed the same original - # exception. - try: - raise TypeError - except: - pass - @app.route('/') - def fails(): - 1/0 - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - assert len(called) == 2 - - def test_before_after_request_order(self): - called = [] - app = flask.Flask(__name__) - @app.before_request - def before1(): - called.append(1) - @app.before_request - def before2(): - called.append(2) - @app.after_request - def after1(response): - called.append(4) - return response - @app.after_request - def after2(response): - called.append(3) - return response - @app.teardown_request - def finish1(exc): - called.append(6) - @app.teardown_request - def finish2(exc): - called.append(5) - @app.route('/') - def index(): - return '42' - rv = app.test_client().get('/') - assert rv.data == '42' - assert called == [1, 2, 3, 4, 5, 6] - - def test_error_handling(self): - app = flask.Flask(__name__) - @app.errorhandler(404) - def not_found(e): - return 'not found', 404 - @app.errorhandler(500) - def internal_server_error(e): - return 'internal server error', 500 - @app.route('/') - def index(): - flask.abort(404) - @app.route('/error') - def error(): - 1 // 0 - c = app.test_client() - rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' - rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data - - def test_before_request_and_routing_errors(self): - app = flask.Flask(__name__) - @app.before_request - def attach_something(): - flask.g.something = 'value' - @app.errorhandler(404) - def return_something(error): - return flask.g.something, 404 - rv = app.test_client().get('/') - assert rv.status_code == 404 - assert rv.data == 'value' - - def test_user_error_handling(self): - class MyException(Exception): - pass - - app = flask.Flask(__name__) - @app.errorhandler(MyException) - def handle_my_exception(e): - assert isinstance(e, MyException) - return '42' - @app.route('/') - def index(): - raise MyException() - - c = app.test_client() - assert c.get('/').data == '42' - - def test_trapping_of_bad_request_key_errors(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/fail') - def fail(): - flask.request.form['missing_key'] - c = app.test_client() - assert c.get('/fail').status_code == 400 - - app.config['TRAP_BAD_REQUEST_ERRORS'] = True - c = app.test_client() - try: - c.get('/fail') - except KeyError, e: - assert isinstance(e, BadRequest) - else: - self.fail('Expected exception') - - def test_trapping_of_all_http_exceptions(self): - app = flask.Flask(__name__) - app.testing = True - app.config['TRAP_HTTP_EXCEPTIONS'] = True - @app.route('/fail') - def fail(): - flask.abort(404) - - c = app.test_client() - try: - c.get('/fail') - except NotFound, e: - pass - else: - self.fail('Expected exception') - - def test_enctype_debug_helper(self): - from flask.debughelpers import DebugFilesKeyError - app = flask.Flask(__name__) - app.debug = True - @app.route('/fail', methods=['POST']) - def index(): - return flask.request.files['foo'].filename - - # with statement is important because we leave an exception on the - # stack otherwise and we want to ensure that this is not the case - # to not negatively affect other tests. - with app.test_client() as c: - try: - c.post('/fail', data={'foo': 'index.txt'}) - except DebugFilesKeyError, e: - assert 'no file contents were transmitted' in str(e) - assert 'This was submitted: "index.txt"' in str(e) - else: - self.fail('Expected exception') - - def test_teardown_on_pop(self): - buffer = [] - app = flask.Flask(__name__) - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - ctx = app.test_request_context() - ctx.push() - assert buffer == [] - ctx.pop() - assert buffer == [None] - - def test_response_creation(self): - app = flask.Flask(__name__) - @app.route('/unicode') - def from_unicode(): - return u'Hällo Wörld' - @app.route('/string') - def from_string(): - return u'Hällo Wörld'.encode('utf-8') - @app.route('/args') - def from_tuple(): - return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' - c = app.test_client() - assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') - assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') - rv = c.get('/args') - assert rv.data == 'Meh' - assert rv.headers['X-Foo'] == 'Testing' - assert rv.status_code == 400 - assert rv.mimetype == 'text/plain' - - def test_make_response(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.make_response() - assert rv.status_code == 200 - assert rv.data == '' - assert rv.mimetype == 'text/html' - - rv = flask.make_response('Awesome') - assert rv.status_code == 200 - assert rv.data == 'Awesome' - assert rv.mimetype == 'text/html' - - rv = flask.make_response('W00t', 404) - assert rv.status_code == 404 - assert rv.data == 'W00t' - assert rv.mimetype == 'text/html' - - def test_url_generation(self): - app = flask.Flask(__name__) - @app.route('/hello/', methods=['POST']) - def hello(): - pass - with app.test_request_context(): - assert flask.url_for('hello', name='test x') == '/hello/test%20x' - assert flask.url_for('hello', name='test x', _external=True) \ - == 'http://localhost/hello/test%20x' - - def test_custom_converters(self): - from werkzeug.routing import BaseConverter - class ListConverter(BaseConverter): - def to_python(self, value): - return value.split(',') - def to_url(self, value): - base_to_url = super(ListConverter, self).to_url - return ','.join(base_to_url(x) for x in value) - app = flask.Flask(__name__) - app.url_map.converters['list'] = ListConverter - @app.route('/') - def index(args): - return '|'.join(args) - c = app.test_client() - assert c.get('/1,2,3').data == '1|2|3' - - def test_static_files(self): - app = flask.Flask(__name__) - rv = app.test_client().get('/static/index.html') - assert rv.status_code == 200 - assert rv.data.strip() == '

    Hello World!

    ' - with app.test_request_context(): - assert flask.url_for('static', filename='index.html') \ - == '/static/index.html' - - def test_none_response(self): - app = flask.Flask(__name__) - @app.route('/') - def test(): - return None - try: - app.test_client().get('/') - except ValueError, e: - assert str(e) == 'View function did not return a response' - pass - else: - assert "Expected ValueError" - - def test_request_locals(self): - self.assertEqual(repr(flask.g), '') - self.assertFalse(flask.g) - - def test_proper_test_request_context(self): - app = flask.Flask(__name__) - app.config.update( - SERVER_NAME='localhost.localdomain:5000' - ) - - @app.route('/') - def index(): - return None - - @app.route('/', subdomain='foo') - def sub(): - return None - - with app.test_request_context('/'): - assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' - - with app.test_request_context('/'): - assert flask.url_for('sub', _external=True) == 'http://foo.localhost.localdomain:5000/' - - try: - with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): - pass - except Exception, e: - assert isinstance(e, ValueError) - assert str(e) == "the server name provided " + \ - "('localhost.localdomain:5000') does not match the " + \ - "server name from the WSGI environment ('localhost')", str(e) - - try: - app.config.update(SERVER_NAME='localhost') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost:80') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - def test_test_app_proper_environ(self): - app = flask.Flask(__name__) - app.config.update( - SERVER_NAME='localhost.localdomain:5000' - ) - @app.route('/') - def index(): - return 'Foo' - - @app.route('/', subdomain='foo') - def subdomain(): - return 'Foo SubDomain' - - try: - rv = app.test_client().get('/') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - rv = app.test_client().get('/', 'http://localhost.localdomain:5000') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - rv = app.test_client().get('/', 'https://localhost.localdomain:5000') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost.localdomain') - rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost.localdomain:443') - rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' - except ValueError, e: - assert str(e) == "the server name provided " + \ - "('localhost.localdomain:443') does not match the " + \ - "server name from the WSGI environment ('localhost.localdomain')", str(e) - - try: - app.config.update(SERVER_NAME='localhost.localdomain') - app.test_client().get('/', 'http://foo.localhost') - except ValueError, e: - assert str(e) == "the server name provided " + \ - "('localhost.localdomain') does not match the " + \ - "server name from the WSGI environment ('foo.localhost')", str(e) - - try: - rv = app.test_client().get('/', 'http://foo.localhost.localdomain') - assert rv.data == 'Foo SubDomain' - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - def test_exception_propagation(self): - def apprunner(configkey): - app = flask.Flask(__name__) - @app.route('/') - def index(): - 1/0 - c = app.test_client() - if config_key is not None: - app.config[config_key] = True - try: - resp = c.get('/') - except Exception: - pass - else: - self.fail('expected exception') - else: - assert c.get('/').status_code == 500 - - # we have to run this test in an isolated thread because if the - # debug flag is set to true and an exception happens the context is - # not torn down. This causes other tests that run after this fail - # when they expect no exception on the stack. - for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None: - t = Thread(target=apprunner, args=(config_key,)) - t.start() - t.join() - - def test_max_content_length(self): - app = flask.Flask(__name__) - app.config['MAX_CONTENT_LENGTH'] = 64 - @app.before_request - def always_first(): - flask.request.form['myfile'] - assert False - @app.route('/accept', methods=['POST']) - def accept_file(): - flask.request.form['myfile'] - assert False - @app.errorhandler(413) - def catcher(error): - return '42' - - c = app.test_client() - rv = c.post('/accept', data={'myfile': 'foo' * 100}) - assert rv.data == '42' - - def test_url_processors(self): - app = flask.Flask(__name__) - - @app.url_defaults - def add_language_code(endpoint, values): - if flask.g.lang_code is not None and \ - app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): - values.setdefault('lang_code', flask.g.lang_code) - - @app.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop('lang_code', None) - - @app.route('//') - def index(): - return flask.url_for('about') - - @app.route('//about') - def about(): - return flask.url_for('something_else') - - @app.route('/foo') - def something_else(): - return flask.url_for('about', lang_code='en') - - c = app.test_client() - - self.assertEqual(c.get('/de/').data, '/de/about') - self.assertEqual(c.get('/de/about').data, '/foo') - self.assertEqual(c.get('/foo').data, '/en/about') - - def test_debug_mode_complains_after_first_request(self): - app = flask.Flask(__name__) - app.debug = True - @app.route('/') - def index(): - return 'Awesome' - self.assert_(not app.got_first_request) - self.assertEqual(app.test_client().get('/').data, 'Awesome') - try: - @app.route('/foo') - def broken(): - return 'Meh' - except AssertionError, e: - self.assert_('A setup function was called' in str(e)) - else: - self.fail('Expected exception') - - app.debug = False - @app.route('/foo') - def working(): - return 'Meh' - self.assertEqual(app.test_client().get('/foo').data, 'Meh') - self.assert_(app.got_first_request) - - def test_before_first_request_functions(self): - got = [] - app = flask.Flask(__name__) - @app.before_first_request - def foo(): - got.append(42) - c = app.test_client() - c.get('/') - self.assertEqual(got, [42]) - c.get('/') - self.assertEqual(got, [42]) - self.assert_(app.got_first_request) - - def test_routing_redirect_debugging(self): - app = flask.Flask(__name__) - app.debug = True - @app.route('/foo/', methods=['GET', 'POST']) - def foo(): - return 'success' - with app.test_client() as c: - try: - c.post('/foo', data={}) - except AssertionError, e: - self.assert_('http://localhost/foo/' in str(e)) - self.assert_('Make sure to directly send your POST-request ' - 'to this URL' in str(e)) - else: - self.fail('Expected exception') - - rv = c.get('/foo', data={}, follow_redirects=True) - self.assertEqual(rv.data, 'success') - - app.debug = False - with app.test_client() as c: - rv = c.post('/foo', data={}, follow_redirects=True) - self.assertEqual(rv.data, 'success') - - -class TestToolsTestCase(FlaskTestCase): - - def test_environ_defaults_from_config(self): - app = flask.Flask(__name__) - app.testing = True - app.config['SERVER_NAME'] = 'example.com:1234' - app.config['APPLICATION_ROOT'] = '/foo' - @app.route('/') - def index(): - return flask.request.url - - ctx = app.test_request_context() - self.assertEqual(ctx.request.url, 'http://example.com:1234/foo/') - with app.test_client() as c: - rv = c.get('/') - self.assertEqual(rv.data, 'http://example.com:1234/foo/') - - def test_environ_defaults(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/') - def index(): - return flask.request.url - - ctx = app.test_request_context() - self.assertEqual(ctx.request.url, 'http://localhost/') - with app.test_client() as c: - rv = c.get('/') - self.assertEqual(rv.data, 'http://localhost/') - - def test_session_transactions(self): - app = flask.Flask(__name__) - app.testing = True - app.secret_key = 'testing' - - @app.route('/') - def index(): - return unicode(flask.session['foo']) - - with app.test_client() as c: - with c.session_transaction() as sess: - self.assertEqual(len(sess), 0) - sess['foo'] = [42] - self.assertEqual(len(sess), 1) - rv = c.get('/') - self.assertEqual(rv.data, '[42]') - - def test_session_transactions_no_null_sessions(self): - app = flask.Flask(__name__) - app.testing = True - - with app.test_client() as c: - try: - with c.session_transaction() as sess: - pass - except RuntimeError, e: - self.assert_('Session backend did not open a session' in str(e)) - else: - self.fail('Expected runtime error') - - def test_session_transactions_keep_context(self): - app = flask.Flask(__name__) - app.testing = True - app.secret_key = 'testing' - - with app.test_client() as c: - rv = c.get('/') - req = flask.request._get_current_object() - with c.session_transaction(): - self.assert_(req is flask.request._get_current_object()) - - def test_test_client_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - flask.g.value = 42 - return 'Hello World!' - - @app.route('/other') - def other(): - 1/0 - - with app.test_client() as c: - resp = c.get('/') - assert flask.g.value == 42 - assert resp.data == 'Hello World!' - assert resp.status_code == 200 - - resp = c.get('/other') - assert not hasattr(flask.g, 'value') - assert 'Internal Server Error' in resp.data - assert resp.status_code == 500 - flask.g.value = 23 - - try: - flask.g.value - except (AttributeError, RuntimeError): - pass - else: - raise AssertionError('some kind of exception expected') - - -class InstanceTestCase(FlaskTestCase): - - def test_explicit_instance_paths(self): - here = os.path.abspath(os.path.dirname(__file__)) - try: - flask.Flask(__name__, instance_path='instance') - except ValueError, e: - self.assert_('must be absolute' in str(e)) - else: - self.fail('Expected value error') - - app = flask.Flask(__name__, instance_path=here) - self.assertEqual(app.instance_path, here) - - def test_uninstalled_module_paths(self): - here = os.path.abspath(os.path.dirname(__file__)) - app = flask.Flask(__name__) - self.assertEqual(app.instance_path, os.path.join(here, 'instance')) - - def test_uninstalled_package_paths(self): - from blueprintapp import app - here = os.path.abspath(os.path.dirname(__file__)) - self.assertEqual(app.instance_path, os.path.join(here, 'instance')) - - def test_installed_module_paths(self): - import types - expected_prefix = os.path.abspath('foo') - mod = types.ModuleType('myapp') - mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_installed_package_paths(self): - import types - expected_prefix = os.path.abspath('foo') - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_prefix_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - def test_egg_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'MyApp.egg', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod - try: - mod.app = flask.Flask(mod.__name__) - self.assertEqual(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) - finally: - sys.modules['myapp'] = None - - -class JSONTestCase(FlaskTestCase): - - def test_json_bad_requests(self): - app = flask.Flask(__name__) - @app.route('/json', methods=['POST']) - def return_json(): - return unicode(flask.request.json) - c = app.test_client() - rv = c.post('/json', data='malformed', content_type='application/json') - self.assertEqual(rv.status_code, 400) - - def test_json_body_encoding(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/') - def index(): - return flask.request.json - - c = app.test_client() - resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), - content_type='application/json; charset=iso-8859-15') - assert resp.data == u'Hällo Wörld'.encode('utf-8') - - def test_jsonify(self): - d = dict(a=23, b=42, c=[1, 2, 3]) - app = flask.Flask(__name__) - @app.route('/kw') - def return_kwargs(): - return flask.jsonify(**d) - @app.route('/dict') - def return_dict(): - return flask.jsonify(d) - c = app.test_client() - for url in '/kw', '/dict': - rv = c.get(url) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d - - def test_json_attr(self): - app = flask.Flask(__name__) - @app.route('/add', methods=['POST']) - def add(): - return unicode(flask.request.json['a'] + flask.request.json['b']) - c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), - content_type='application/json') - assert rv.data == '3' - - def test_template_escaping(self): - app = flask.Flask(__name__) - render = flask.render_template_string - with app.test_request_context(): - rv = render('{{ ""|tojson|safe }}') - assert rv == '"<\\/script>"' - rv = render('{{ "<\0/script>"|tojson|safe }}') - assert rv == '"<\\u0000\\/script>"' - - def test_modified_url_encoding(self): - class ModifiedRequest(flask.Request): - url_charset = 'euc-kr' - app = flask.Flask(__name__) - app.request_class = ModifiedRequest - app.url_map.charset = 'euc-kr' - - @app.route('/') - def index(): - return flask.request.args['foo'] - - rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) - assert rv.status_code == 200 - assert rv.data == u'정상처리'.encode('utf-8') - - if not has_encoding('euc-kr'): - test_modified_url_encoding = None - - -class TemplatingTestCase(FlaskTestCase): - - def test_context_processing(self): - app = flask.Flask(__name__) - @app.context_processor - def context_processor(): - return {'injected_value': 42} - @app.route('/') - def index(): - return flask.render_template('context_template.html', value=23) - rv = app.test_client().get('/') - assert rv.data == '

    23|42' - - def test_original_win(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.render_template_string('{{ config }}', config=42) - rv = app.test_client().get('/') - assert rv.data == '42' - - def test_standard_context(self): - app = flask.Flask(__name__) - app.secret_key = 'development key' - @app.route('/') - def index(): - flask.g.foo = 23 - flask.session['test'] = 'aha' - return flask.render_template_string(''' - {{ request.args.foo }} - {{ g.foo }} - {{ config.DEBUG }} - {{ session.test }} - ''') - rv = app.test_client().get('/?foo=42') - assert rv.data.split() == ['42', '23', 'False', 'aha'] - - def test_escaping(self): - text = '

    Hello World!' - app = flask.Flask(__name__) - @app.route('/') - def index(): - return flask.render_template('escaping_template.html', text=text, - html=flask.Markup(text)) - lines = app.test_client().get('/').data.splitlines() - assert lines == [ - '<p>Hello World!', - '

    Hello World!', - '

    Hello World!', - '

    Hello World!', - '<p>Hello World!', - '

    Hello World!' - ] - - def test_no_escaping(self): - app = flask.Flask(__name__) - with app.test_request_context(): - assert flask.render_template_string('{{ foo }}', - foo='') == '' - assert flask.render_template('mail.txt', foo='') \ - == ' Mail' - - def test_macros(self): - app = flask.Flask(__name__) - with app.test_request_context(): - macro = flask.get_template_attribute('_macro.html', 'hello') - assert macro('World') == 'Hello World!' - - def test_template_filter(self): - app = flask.Flask(__name__) - @app.template_filter() - def my_reverse(s): - return s[::-1] - assert 'my_reverse' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['my_reverse'] == my_reverse - assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' - - def test_template_filter_with_name(self): - app = flask.Flask(__name__) - @app.template_filter('strrev') - def my_reverse(s): - return s[::-1] - assert 'strrev' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['strrev'] == my_reverse - assert app.jinja_env.filters['strrev']('abcd') == 'dcba' - - def test_template_filter_with_template(self): - app = flask.Flask(__name__) - @app.template_filter() - def super_reverse(s): - return s[::-1] - @app.route('/') - def index(): - return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') - assert rv.data == 'dcba' - - def test_template_filter_with_name_and_template(self): - app = flask.Flask(__name__) - @app.template_filter('super_reverse') - def my_reverse(s): - return s[::-1] - @app.route('/') - def index(): - return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') - assert rv.data == 'dcba' - - def test_custom_template_loader(self): - class MyFlask(flask.Flask): - def create_global_jinja_loader(self): - from jinja2 import DictLoader - return DictLoader({'index.html': 'Hello Custom World!'}) - app = MyFlask(__name__) - @app.route('/') - def index(): - return flask.render_template('index.html') - c = app.test_client() - rv = c.get('/') - assert rv.data == 'Hello Custom World!' - - -class ModuleTestCase(FlaskTestCase): - - @emits_module_deprecation_warning - def test_basic_module(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @admin.route('/') - def admin_index(): - return 'admin index' - @admin.route('/login') - def admin_login(): - return 'admin login' - @admin.route('/logout') - def admin_logout(): - return 'admin logout' - @app.route('/') - def index(): - return 'the index' - app.register_module(admin) - c = app.test_client() - assert c.get('/').data == 'the index' - assert c.get('/admin/').data == 'admin index' - assert c.get('/admin/login').data == 'admin login' - assert c.get('/admin/logout').data == 'admin logout' - - @emits_module_deprecation_warning - def test_default_endpoint_name(self): - app = flask.Flask(__name__) - mod = flask.Module(__name__, 'frontend') - def index(): - return 'Awesome' - mod.add_url_rule('/', view_func=index) - app.register_module(mod) - rv = app.test_client().get('/') - assert rv.data == 'Awesome' - with app.test_request_context(): - assert flask.url_for('frontend.index') == '/' - - @emits_module_deprecation_warning - def test_request_processing(self): - catched = [] - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @admin.before_request - def before_admin_request(): - catched.append('before-admin') - @admin.after_request - def after_admin_request(response): - catched.append('after-admin') - return response - @admin.route('/') - def admin_index(): - return 'the admin' - @app.before_request - def before_request(): - catched.append('before-app') - @app.after_request - def after_request(response): - catched.append('after-app') - return response - @app.route('/') - def index(): - return 'the index' - app.register_module(admin) - c = app.test_client() - - assert c.get('/').data == 'the index' - assert catched == ['before-app', 'after-app'] - del catched[:] - - assert c.get('/admin/').data == 'the admin' - assert catched == ['before-app', 'before-admin', - 'after-admin', 'after-app'] - - @emits_module_deprecation_warning - def test_context_processors(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin', url_prefix='/admin') - @app.context_processor - def inject_all_regualr(): - return {'a': 1} - @admin.context_processor - def inject_admin(): - return {'b': 2} - @admin.app_context_processor - def inject_all_module(): - return {'c': 3} - @app.route('/') - def index(): - return flask.render_template_string('{{ a }}{{ b }}{{ c }}') - @admin.route('/') - def admin_index(): - return flask.render_template_string('{{ a }}{{ b }}{{ c }}') - app.register_module(admin) - c = app.test_client() - assert c.get('/').data == '13' - assert c.get('/admin/').data == '123' - - @emits_module_deprecation_warning - def test_late_binding(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin') - @admin.route('/') - def index(): - return '42' - app.register_module(admin, url_prefix='/admin') - assert app.test_client().get('/admin/').data == '42' - - @emits_module_deprecation_warning - def test_error_handling(self): - app = flask.Flask(__name__) - admin = flask.Module(__name__, 'admin') - @admin.app_errorhandler(404) - def not_found(e): - return 'not found', 404 - @admin.app_errorhandler(500) - def internal_server_error(e): - return 'internal server error', 500 - @admin.route('/') - def index(): - flask.abort(404) - @admin.route('/error') - def error(): - 1 // 0 - app.register_module(admin) - c = app.test_client() - rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' - rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data - - def test_templates_and_static(self): - app = moduleapp - app.testing = True - c = app.test_client() - - rv = c.get('/') - assert rv.data == 'Hello from the Frontend' - rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' - rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' - - with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' - - with app.test_request_context(): - try: - flask.render_template('missing.html') - except TemplateNotFound, e: - assert e.name == 'missing.html' - else: - assert 0, 'expected exception' - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' - - def test_safe_access(self): - app = moduleapp - - with app.test_request_context(): - f = app.view_functions['admin.static'] - - try: - f('/etc/passwd') - except NotFound: - pass - else: - assert 0, 'expected exception' - try: - f('../__init__.py') - except NotFound: - pass - else: - assert 0, 'expected exception' - - # testcase for a security issue that may exist on windows systems - import os - import ntpath - old_path = os.path - os.path = ntpath - try: - try: - f('..\\__init__.py') - except NotFound: - pass - else: - assert 0, 'expected exception' - finally: - os.path = old_path - - @emits_module_deprecation_warning - def test_endpoint_decorator(self): - from werkzeug.routing import Submount, Rule - from flask import Module - - app = flask.Flask(__name__) - app.testing = True - app.url_map.add(Submount('/foo', [ - Rule('/bar', endpoint='bar'), - Rule('/', endpoint='index') - ])) - module = Module(__name__, __name__) - - @module.endpoint('bar') - def bar(): - return 'bar' - - @module.endpoint('index') - def index(): - return 'index' - - app.register_module(module) - - c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' - - -class BlueprintTestCase(FlaskTestCase): - - def test_blueprint_specific_error_handling(self): - frontend = flask.Blueprint('frontend', __name__) - backend = flask.Blueprint('backend', __name__) - sideend = flask.Blueprint('sideend', __name__) - - @frontend.errorhandler(403) - def frontend_forbidden(e): - return 'frontend says no', 403 - - @frontend.route('/frontend-no') - def frontend_no(): - flask.abort(403) - - @backend.errorhandler(403) - def backend_forbidden(e): - return 'backend says no', 403 - - @backend.route('/backend-no') - def backend_no(): - flask.abort(403) - - @sideend.route('/what-is-a-sideend') - def sideend_no(): - flask.abort(403) - - app = flask.Flask(__name__) - app.register_blueprint(frontend) - app.register_blueprint(backend) - app.register_blueprint(sideend) - - @app.errorhandler(403) - def app_forbidden(e): - return 'application itself says no', 403 - - c = app.test_client() - - assert c.get('/frontend-no').data == 'frontend says no' - assert c.get('/backend-no').data == 'backend says no' - assert c.get('/what-is-a-sideend').data == 'application itself says no' - - def test_blueprint_url_definitions(self): - bp = flask.Blueprint('test', __name__) - - @bp.route('/foo', defaults={'baz': 42}) - def foo(bar, baz): - return '%s/%d' % (bar, baz) - - @bp.route('/bar') - def bar(bar): - return unicode(bar) - - app = flask.Flask(__name__) - app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) - app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) - - c = app.test_client() - self.assertEqual(c.get('/1/foo').data, u'23/42') - self.assertEqual(c.get('/2/foo').data, u'19/42') - self.assertEqual(c.get('/1/bar').data, u'23') - self.assertEqual(c.get('/2/bar').data, u'19') - - def test_blueprint_url_processors(self): - bp = flask.Blueprint('frontend', __name__, url_prefix='/') - - @bp.url_defaults - def add_language_code(endpoint, values): - values.setdefault('lang_code', flask.g.lang_code) - - @bp.url_value_preprocessor - def pull_lang_code(endpoint, values): - flask.g.lang_code = values.pop('lang_code') - - @bp.route('/') - def index(): - return flask.url_for('.about') - - @bp.route('/about') - def about(): - return flask.url_for('.index') - - app = flask.Flask(__name__) - app.register_blueprint(bp) - - c = app.test_client() - - self.assertEqual(c.get('/de/').data, '/de/about') - self.assertEqual(c.get('/de/about').data, '/de/') - - def test_templates_and_static(self): - from blueprintapp import app - c = app.test_client() - - rv = c.get('/') - assert rv.data == 'Hello from the Frontend' - rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' - rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' - rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' - - with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' - - with app.test_request_context(): - try: - flask.render_template('missing.html') - except TemplateNotFound, e: - assert e.name == 'missing.html' - else: - assert 0, 'expected exception' - - with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' - - def test_templates_list(self): - from blueprintapp import app - templates = sorted(app.jinja_env.list_templates()) - self.assertEqual(templates, ['admin/index.html', - 'frontend/index.html']) - - def test_dotted_names(self): - frontend = flask.Blueprint('myapp.frontend', __name__) - backend = flask.Blueprint('myapp.backend', __name__) - - @frontend.route('/fe') - def frontend_index(): - return flask.url_for('myapp.backend.backend_index') - - @frontend.route('/fe2') - def frontend_page2(): - return flask.url_for('.frontend_index') - - @backend.route('/be') - def backend_index(): - return flask.url_for('myapp.frontend.frontend_index') - - app = flask.Flask(__name__) - app.register_blueprint(frontend) - app.register_blueprint(backend) - - c = app.test_client() - self.assertEqual(c.get('/fe').data.strip(), '/be') - self.assertEqual(c.get('/fe2').data.strip(), '/fe') - self.assertEqual(c.get('/be').data.strip(), '/fe') - - def test_empty_url_defaults(self): - bp = flask.Blueprint('bp', __name__) - - @bp.route('/', defaults={'page': 1}) - @bp.route('/page/') - def something(page): - return str(page) - - app = flask.Flask(__name__) - app.register_blueprint(bp) - - c = app.test_client() - self.assertEqual(c.get('/').data, '1') - self.assertEqual(c.get('/page/2').data, '2') - - -class SendfileTestCase(FlaskTestCase): - - def test_send_file_regular(self): - app = flask.Flask(__name__) - with app.test_request_context(): - rv = flask.send_file('static/index.html') - assert rv.direct_passthrough - assert rv.mimetype == 'text/html' - with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - - def test_send_file_xsendfile(self): - app = flask.Flask(__name__) - app.use_x_sendfile = True - with app.test_request_context(): - rv = flask.send_file('static/index.html') - assert rv.direct_passthrough - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') - assert rv.mimetype == 'text/html' - - def test_send_file_object(self): - app = flask.Flask(__name__) - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - assert rv.mimetype == 'text/html' - # mimetypes + etag - assert len(captured) == 2 - - app.use_x_sendfile = True - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) - assert rv.mimetype == 'text/html' - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') - # mimetypes + etag - assert len(captured) == 2 - - app.use_x_sendfile = False - with app.test_request_context(): - with catch_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f) - assert rv.data == 'Test' - assert rv.mimetype == 'application/octet-stream' - # etags - assert len(captured) == 1 - with catch_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f, mimetype='text/plain') - assert rv.data == 'Test' - assert rv.mimetype == 'text/plain' - # etags - assert len(captured) == 1 - - app.use_x_sendfile = True - with catch_warnings() as captured: - with app.test_request_context(): - f = StringIO('Test') - rv = flask.send_file(f) - assert 'x-sendfile' not in rv.headers - # etags - assert len(captured) == 1 - - def test_attachment(self): - app = flask.Flask(__name__) - with catch_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f, as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - # mimetypes + etag - assert len(captured) == 2 - - with app.test_request_context(): - assert options['filename'] == 'index.html' - rv = flask.send_file('static/index.html', as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.html' - - with app.test_request_context(): - rv = flask.send_file(StringIO('Test'), as_attachment=True, - attachment_filename='index.txt', - add_etags=False) - assert rv.mimetype == 'text/plain' - value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.txt' - - -class LoggingTestCase(FlaskTestCase): - - def test_logger_cache(self): - app = flask.Flask(__name__) - logger1 = app.logger - assert app.logger is logger1 - assert logger1.name == __name__ - app.logger_name = __name__ + '/test_logger_cache' - assert app.logger is not logger1 - - def test_debug_log(self): - app = flask.Flask(__name__) - app.debug = True - - @app.route('/') - def index(): - app.logger.warning('the standard library is dead') - app.logger.debug('this is a debug statement') - return '' - - @app.route('/exc') - def exc(): - 1/0 - - with app.test_client() as c: - with catch_stderr() as err: - c.get('/') - out = err.getvalue() - assert 'WARNING in flask_tests [' in out - assert 'flask_tests.py' in out - assert 'the standard library is dead' in out - assert 'this is a debug statement' in out - - with catch_stderr() as err: - try: - c.get('/exc') - except ZeroDivisionError: - pass - else: - assert False, 'debug log ate the exception' - - def test_exception_logging(self): - out = StringIO() - app = flask.Flask(__name__) - app.logger_name = 'flask_tests/test_exception_logging' - app.logger.addHandler(StreamHandler(out)) - - @app.route('/') - def index(): - 1/0 - - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert 'Internal Server Error' in rv.data - - err = out.getvalue() - assert 'Exception on / [GET]' in err - assert 'Traceback (most recent call last):' in err - assert '1/0' in err - assert 'ZeroDivisionError:' in err - - def test_processor_exceptions(self): - app = flask.Flask(__name__) - @app.before_request - def before_request(): - if trigger == 'before': - 1/0 - @app.after_request - def after_request(response): - if trigger == 'after': - 1/0 - return response - @app.route('/') - def index(): - return 'Foo' - @app.errorhandler(500) - def internal_server_error(e): - return 'Hello Server Error', 500 - for trigger in 'before', 'after': - rv = app.test_client().get('/') - assert rv.status_code == 500 - assert rv.data == 'Hello Server Error' - - -class ConfigTestCase(FlaskTestCase): - - def common_object_test(self, app): - assert app.secret_key == 'devkey' - assert app.config['TEST_KEY'] == 'foo' - assert 'ConfigTestCase' not in app.config - - def test_config_from_file(self): - app = flask.Flask(__name__) - app.config.from_pyfile('flask_tests.py') - self.common_object_test(app) - - def test_config_from_object(self): - app = flask.Flask(__name__) - app.config.from_object(__name__) - self.common_object_test(app) - - def test_config_from_class(self): - class Base(object): - TEST_KEY = 'foo' - class Test(Base): - SECRET_KEY = 'devkey' - app = flask.Flask(__name__) - app.config.from_object(Test) - self.common_object_test(app) - - def test_config_from_envvar(self): - import os - env = os.environ - try: - os.environ = {} - app = flask.Flask(__name__) - try: - app.config.from_envvar('FOO_SETTINGS') - except RuntimeError, e: - assert "'FOO_SETTINGS' is not set" in str(e) - else: - assert 0, 'expected exception' - assert not app.config.from_envvar('FOO_SETTINGS', silent=True) - - os.environ = {'FOO_SETTINGS': 'flask_tests.py'} - assert app.config.from_envvar('FOO_SETTINGS') - self.common_object_test(app) - finally: - os.environ = env - - def test_config_missing(self): - app = flask.Flask(__name__) - try: - app.config.from_pyfile('missing.cfg') - except IOError, e: - msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.cfg'") - else: - assert 0, 'expected config' - assert not app.config.from_pyfile('missing.cfg', silent=True) - - -class SubdomainTestCase(FlaskTestCase): - - def test_basic_support(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' - @app.route('/') - def normal_index(): - return 'normal index' - @app.route('/', subdomain='test') - def test_index(): - return 'test index' - - c = app.test_client() - rv = c.get('/', 'http://localhost/') - assert rv.data == 'normal index' - - rv = c.get('/', 'http://test.localhost/') - assert rv.data == 'test index' - - @emits_module_deprecation_warning - def test_module_static_path_subdomain(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'example.com' - from subdomaintestmodule import mod - app.register_module(mod) - c = app.test_client() - rv = c.get('/static/hello.txt', 'http://foo.example.com/') - assert rv.data.strip() == 'Hello Subdomain' - - def test_subdomain_matching(self): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' - @app.route('/', subdomain='') - def index(user): - return 'index for %s' % user - - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost/') - assert rv.data == 'index for mitsuhiko' - - @emits_module_deprecation_warning - def test_module_subdomain_support(self): - app = flask.Flask(__name__) - mod = flask.Module(__name__, 'test', subdomain='testing') - app.config['SERVER_NAME'] = 'localhost' - - @mod.route('/test') - def test(): - return 'Test' - - @mod.route('/outside', subdomain='xtesting') - def bar(): - return 'Outside' - - app.register_module(mod) - - c = app.test_client() - rv = c.get('/test', 'http://testing.localhost/') - assert rv.data == 'Test' - rv = c.get('/outside', 'http://xtesting.localhost/') - assert rv.data == 'Outside' - - -class TestSignals(FlaskTestCase): - - def test_template_rendered(self): - app = flask.Flask(__name__) - - @app.route('/') - def index(): - return flask.render_template('simple_template.html', whiskey=42) - - recorded = [] - def record(sender, template, context): - recorded.append((template, context)) - - flask.template_rendered.connect(record, app) - try: - app.test_client().get('/') - assert len(recorded) == 1 - template, context = recorded[0] - assert template.name == 'simple_template.html' - assert context['whiskey'] == 42 - finally: - flask.template_rendered.disconnect(record, app) - - def test_request_signals(self): - app = flask.Flask(__name__) - calls = [] - - def before_request_signal(sender): - calls.append('before-signal') - - def after_request_signal(sender, response): - assert response.data == 'stuff' - calls.append('after-signal') - - @app.before_request - def before_request_handler(): - calls.append('before-handler') - - @app.after_request - def after_request_handler(response): - calls.append('after-handler') - response.data = 'stuff' - return response - - @app.route('/') - def index(): - calls.append('handler') - return 'ignored anyway' - - flask.request_started.connect(before_request_signal, app) - flask.request_finished.connect(after_request_signal, app) - - try: - rv = app.test_client().get('/') - assert rv.data == 'stuff' - - assert calls == ['before-signal', 'before-handler', - 'handler', 'after-handler', - 'after-signal'] - finally: - flask.request_started.disconnect(before_request_signal, app) - flask.request_finished.disconnect(after_request_signal, app) - - def test_request_exception_signal(self): - app = flask.Flask(__name__) - recorded = [] - - @app.route('/') - def index(): - 1/0 - - def record(sender, exception): - recorded.append(exception) - - flask.got_request_exception.connect(record, app) - try: - assert app.test_client().get('/').status_code == 500 - assert len(recorded) == 1 - assert isinstance(recorded[0], ZeroDivisionError) - finally: - flask.got_request_exception.disconnect(record, app) - - -class ViewTestCase(FlaskTestCase): - - def common_test(self, app): - c = app.test_client() - - self.assertEqual(c.get('/').data, 'GET') - self.assertEqual(c.post('/').data, 'POST') - self.assertEqual(c.put('/').status_code, 405) - meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) - self.assertEqual(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) - - def test_basic_view(self): - app = flask.Flask(__name__) - - class Index(flask.views.View): - methods = ['GET', 'POST'] - def dispatch_request(self): - return flask.request.method - - app.add_url_rule('/', view_func=Index.as_view('index')) - self.common_test(app) - - def test_method_based_view(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - return 'GET' - def post(self): - return 'POST' - - app.add_url_rule('/', view_func=Index.as_view('index')) - - self.common_test(app) - - def test_view_patching(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - 1/0 - def post(self): - 1/0 - - class Other(Index): - def get(self): - return 'GET' - def post(self): - return 'POST' - - view = Index.as_view('index') - view.view_class = Other - app.add_url_rule('/', view_func=view) - self.common_test(app) - - def test_view_inheritance(self): - app = flask.Flask(__name__) - - class Index(flask.views.MethodView): - def get(self): - return 'GET' - def post(self): - return 'POST' - - class BetterIndex(Index): - def delete(self): - return 'DELETE' - - app.add_url_rule('/', view_func=BetterIndex.as_view('index')) - c = app.test_client() - - meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) - self.assertEqual(sorted(meths), ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST']) - - def test_view_decorators(self): - app = flask.Flask(__name__) - - def add_x_parachute(f): - def new_function(*args, **kwargs): - resp = flask.make_response(f(*args, **kwargs)) - resp.headers['X-Parachute'] = 'awesome' - return resp - return new_function - - class Index(flask.views.View): - decorators = [add_x_parachute] - def dispatch_request(self): - return 'Awesome' - - app.add_url_rule('/', view_func=Index.as_view('index')) - c = app.test_client() - rv = c.get('/') - self.assertEqual(rv.headers['X-Parachute'], 'awesome') - self.assertEqual(rv.data, 'Awesome') - - -class DeprecationsTestCase(FlaskTestCase): - - def test_init_jinja_globals(self): - class MyFlask(flask.Flask): - def init_jinja_globals(self): - self.jinja_env.globals['foo'] = '42' - - with catch_warnings() as log: - app = MyFlask(__name__) - @app.route('/') - def foo(): - return app.jinja_env.globals['foo'] - - c = app.test_client() - assert c.get('/').data == '42' - assert len(log) == 1 - assert 'init_jinja_globals' in str(log[0]['message']) - - -def suite(): - from minitwit_tests import MiniTwitTestCase - from flaskr_tests import FlaskrTestCase - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ContextTestCase)) - suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) - suite.addTest(unittest.makeSuite(TemplatingTestCase)) - suite.addTest(unittest.makeSuite(ModuleTestCase)) - suite.addTest(unittest.makeSuite(BlueprintTestCase)) - suite.addTest(unittest.makeSuite(SendfileTestCase)) - suite.addTest(unittest.makeSuite(LoggingTestCase)) - suite.addTest(unittest.makeSuite(ConfigTestCase)) - suite.addTest(unittest.makeSuite(SubdomainTestCase)) - suite.addTest(unittest.makeSuite(ViewTestCase)) - suite.addTest(unittest.makeSuite(DeprecationsTestCase)) - suite.addTest(unittest.makeSuite(TestToolsTestCase)) - suite.addTest(unittest.makeSuite(InstanceTestCase)) - if flask.json_available: - suite.addTest(unittest.makeSuite(JSONTestCase)) - if flask.signals_available: - suite.addTest(unittest.makeSuite(TestSignals)) - suite.addTest(unittest.makeSuite(MiniTwitTestCase)) - suite.addTest(unittest.makeSuite(FlaskrTestCase)) - return suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') From 12d74be7d7f62400663911d94783320bdfba86d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:24:10 +0100 Subject: [PATCH 0789/3747] Added better test runner --- run-tests.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 run-tests.py diff --git a/run-tests.py b/run-tests.py new file mode 100644 index 00000000..96db7f9a --- /dev/null +++ b/run-tests.py @@ -0,0 +1,57 @@ +import sys +import unittest +from unittest.loader import TestLoader +from flask.testsuite import suite + +common_prefix = suite.__module__ + '.' + + +def find_all_tests(): + suites = [suite()] + while suites: + s = suites.pop() + try: + suites.extend(s) + except TypeError: + yield s + + +def find_all_tests_with_name(): + for testcase in find_all_tests(): + yield testcase, '%s.%s.%s' % ( + testcase.__class__.__module__, + testcase.__class__.__name__, + testcase._testMethodName + ) + + +class BetterLoader(TestLoader): + + def loadTestsFromName(self, name, module=None): + if name == 'suite': + return suite() + for testcase, testname in find_all_tests_with_name(): + if testname == name: + return testcase + if testname.startswith(common_prefix): + if testname[len(common_prefix):] == name: + return testcase + + all_results = [] + for testcase, testname in find_all_tests_with_name(): + if testname.endswith('.' + name): + all_results.append((testcase, testname)) + + if len(all_results) == 1: + return all_results[0][0] + elif not len(all_results): + error = 'could not find testcase "%s"' % name + else: + error = 'Too many matches: for "%s"\n%s' % \ + (name, '\n'.join(' - ' + n for c, n in all_results)) + + print >> sys.stderr, 'Error: %s' % error + sys.exit(1) + + +unittest.main(testLoader=BetterLoader(), defaultTest='suite') From 2b830af2efb4523fa2e2e9b9dfcc33212effe2e6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:24:42 +0100 Subject: [PATCH 0790/3747] Use the better test runner in the makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b14422f..43f47275 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: clean-pyc test test: - python setup.py test + python run-tests.py audit: python setup.py audit From 3069e2d7f73f80ebc342029616a5cbc5381cfdf1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:38:43 +0100 Subject: [PATCH 0791/3747] Fight the generic asserts! --- flask/testsuite/basic.py | 212 ++++++++++++++++---------------- flask/testsuite/blueprints.py | 74 +++++------ flask/testsuite/config.py | 4 +- flask/testsuite/deprecations.py | 4 +- flask/testsuite/helpers.py | 78 ++++++------ flask/testsuite/signals.py | 18 +-- flask/testsuite/templating.py | 26 ++-- flask/testsuite/testing.py | 8 +- 8 files changed, 212 insertions(+), 212 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index c55881e5..8c2be901 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -26,8 +26,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(): return 'Hello World' rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - assert rv.data == '' + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) + self.assert_equal(rv.data, '') def test_options_on_multiple_rules(self): app = flask.Flask(__name__) @@ -38,7 +38,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index_put(): return 'Aha!' rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) def test_options_handling_disabled(self): app = flask.Flask(__name__) @@ -47,7 +47,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): index.provide_automatic_options = False app.route('/')(index) rv = app.test_client().open('/', method='OPTIONS') - assert rv.status_code == 405 + self.assert_equal(rv.status_code, 405) app = flask.Flask(__name__) def index2(): @@ -55,7 +55,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): index2.provide_automatic_options = True app.route('/', methods=['OPTIONS'])(index2) rv = app.test_client().open('/', method='OPTIONS') - assert sorted(rv.allow) == ['OPTIONS'] + self.assert_equal(sorted(rv.allow), ['OPTIONS']) def test_request_dispatching(self): app = flask.Flask(__name__) @@ -67,18 +67,18 @@ class BasicFunctionalityTestCase(FlaskTestCase): return flask.request.method c = app.test_client() - assert c.get('/').data == 'GET' + self.assert_equal(c.get('/').data, 'GET') rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') - assert rv.status_code == 200 + self.assert_equal(rv.status_code, 200) assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) def test_url_mapping(self): app = flask.Flask(__name__) @@ -91,18 +91,18 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) c = app.test_client() - assert c.get('/').data == 'GET' + self.assert_equal(c.get('/').data, 'GET') rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') - assert rv.status_code == 200 + self.assert_equal(rv.status_code, 200) assert not rv.data # head truncates - assert c.post('/more').data == 'POST' - assert c.get('/more').data == 'GET' + self.assert_equal(c.post('/more').data, 'POST') + self.assert_equal(c.get('/more').data, 'GET') rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] + self.assert_equal(rv.status_code, 405) + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) def test_werkzeug_routing(self): from werkzeug.routing import Submount, Rule @@ -119,8 +119,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.view_functions['index'] = index c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') def test_endpoint_decorator(self): from werkzeug.routing import Submount, Rule @@ -139,8 +139,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'index' c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') def test_session(self): app = flask.Flask(__name__) @@ -154,8 +154,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): return flask.session['value'] c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == 'value set' - assert c.get('/get').data == '42' + self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set') + self.assert_equal(c.get('/get').data, '42') def test_session_using_server_name(self): app = flask.Flask(__name__) @@ -241,12 +241,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime - assert expires.year == expected.year - assert expires.month == expected.month - assert expires.day == expected.day + self.assert_equal(expires.year, expected.year) + self.assert_equal(expires.month, expected.month) + self.assert_equal(expires.day, expected.day) rv = client.get('/test') - assert rv.data == 'True' + self.assert_equal(rv.data, 'True') permanent = False rv = app.test_client().get('/') @@ -264,7 +264,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session.modified = False flask.flash('Zip') assert flask.session.modified - assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] + self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) def test_extended_flashing(self): app = flask.Flask(__name__) @@ -280,16 +280,16 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/test') def test(): messages = flask.get_flashed_messages(with_categories=True) - assert len(messages) == 3 - assert messages[0] == ('message', u'Hello World') - assert messages[1] == ('error', u'Hello World') - assert messages[2] == ('warning', flask.Markup(u'Testing')) + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], ('message', u'Hello World')) + self.assert_equal(messages[1], ('error', u'Hello World')) + self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) return '' messages = flask.get_flashed_messages() - assert len(messages) == 3 - assert messages[0] == u'Hello World' - assert messages[1] == u'Hello World' - assert messages[2] == flask.Markup(u'Testing') + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], u'Hello World') + self.assert_equal(messages[1], u'Hello World') + self.assert_equal(messages[2], flask.Markup(u'Testing')) c = app.test_client() c.get('/') @@ -314,7 +314,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): assert 'after' not in evts rv = app.test_client().get('/').data assert 'after' in evts - assert rv == 'request|after' + self.assert_equal(rv, 'request|after') def test_teardown_request_handler(self): called = [] @@ -327,9 +327,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): def root(): return "Response" rv = app.test_client().get('/') - assert rv.status_code == 200 + self.assert_equal(rv.status_code, 200) assert 'Response' in rv.data - assert len(called) == 1 + self.assert_equal(len(called), 1) def test_teardown_request_handler_debug_mode(self): called = [] @@ -343,16 +343,16 @@ class BasicFunctionalityTestCase(FlaskTestCase): def root(): return "Response" rv = app.test_client().get('/') - assert rv.status_code == 200 + self.assert_equal(rv.status_code, 200) assert 'Response' in rv.data - assert len(called) == 1 + self.assert_equal(len(called), 1) def test_teardown_request_handler_error(self): called = [] app = flask.Flask(__name__) @app.teardown_request def teardown_request1(exc): - assert type(exc) == ZeroDivisionError + self.assert_equal(type(exc), ZeroDivisionError) called.append(True) # This raises a new error and blows away sys.exc_info(), so we can # test that all teardown_requests get passed the same original @@ -363,7 +363,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): pass @app.teardown_request def teardown_request2(exc): - assert type(exc) == ZeroDivisionError + self.assert_equal(type(exc), ZeroDivisionError) called.append(True) # This raises a new error and blows away sys.exc_info(), so we can # test that all teardown_requests get passed the same original @@ -376,9 +376,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): def fails(): 1/0 rv = app.test_client().get('/') - assert rv.status_code == 500 + self.assert_equal(rv.status_code, 500) assert 'Internal Server Error' in rv.data - assert len(called) == 2 + self.assert_equal(len(called), 2) def test_before_after_request_order(self): called = [] @@ -407,8 +407,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(): return '42' rv = app.test_client().get('/') - assert rv.data == '42' - assert called == [1, 2, 3, 4, 5, 6] + self.assert_equal(rv.data, '42') + self.assert_equal(called, [1, 2, 3, 4, 5, 6]) def test_error_handling(self): app = flask.Flask(__name__) @@ -426,11 +426,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1 // 0 c = app.test_client() rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) def test_before_request_and_routing_errors(self): app = flask.Flask(__name__) @@ -441,8 +441,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): def return_something(error): return flask.g.something, 404 rv = app.test_client().get('/') - assert rv.status_code == 404 - assert rv.data == 'value' + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'value') def test_user_error_handling(self): class MyException(Exception): @@ -458,7 +458,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): raise MyException() c = app.test_client() - assert c.get('/').data == '42' + self.assert_equal(c.get('/').data, '42') def test_trapping_of_bad_request_key_errors(self): app = flask.Flask(__name__) @@ -467,7 +467,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def fail(): flask.request.form['missing_key'] c = app.test_client() - assert c.get('/fail').status_code == 400 + self.assert_equal(c.get('/fail').status_code, 400) app.config['TRAP_BAD_REQUEST_ERRORS'] = True c = app.test_client() @@ -523,9 +523,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): ctx = app.test_request_context() ctx.push() - assert buffer == [] + self.assert_equal(buffer, []) ctx.pop() - assert buffer == [None] + self.assert_equal(buffer, [None]) def test_response_creation(self): app = flask.Flask(__name__) @@ -539,31 +539,31 @@ class BasicFunctionalityTestCase(FlaskTestCase): def from_tuple(): return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' c = app.test_client() - assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') - assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') + self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) + self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) rv = c.get('/args') - assert rv.data == 'Meh' - assert rv.headers['X-Foo'] == 'Testing' - assert rv.status_code == 400 - assert rv.mimetype == 'text/plain' + self.assert_equal(rv.data, 'Meh') + self.assert_equal(rv.headers['X-Foo'], 'Testing') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'text/plain') def test_make_response(self): app = flask.Flask(__name__) with app.test_request_context(): rv = flask.make_response() - assert rv.status_code == 200 - assert rv.data == '' - assert rv.mimetype == 'text/html' + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, '') + self.assert_equal(rv.mimetype, 'text/html') rv = flask.make_response('Awesome') - assert rv.status_code == 200 - assert rv.data == 'Awesome' - assert rv.mimetype == 'text/html' + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.mimetype, 'text/html') rv = flask.make_response('W00t', 404) - assert rv.status_code == 404 - assert rv.data == 'W00t' - assert rv.mimetype == 'text/html' + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'W00t') + self.assert_equal(rv.mimetype, 'text/html') def test_url_generation(self): app = flask.Flask(__name__) @@ -571,7 +571,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def hello(): pass with app.test_request_context(): - assert flask.url_for('hello', name='test x') == '/hello/test%20x' + self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x') assert flask.url_for('hello', name='test x', _external=True) \ == 'http://localhost/hello/test%20x' @@ -589,13 +589,13 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(args): return '|'.join(args) c = app.test_client() - assert c.get('/1,2,3').data == '1|2|3' + self.assert_equal(c.get('/1,2,3').data, '1|2|3') def test_static_files(self): app = flask.Flask(__name__) rv = app.test_client().get('/static/index.html') - assert rv.status_code == 200 - assert rv.data.strip() == '

    Hello World!

    ' + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data.strip(), '

    Hello World!

    ') with app.test_request_context(): assert flask.url_for('static', filename='index.html') \ == '/static/index.html' @@ -608,7 +608,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: app.test_client().get('/') except ValueError, e: - assert str(e) == 'View function did not return a response' + self.assert_equal(str(e), 'View function did not return a response') pass else: assert "Expected ValueError" @@ -632,19 +632,19 @@ class BasicFunctionalityTestCase(FlaskTestCase): return None with app.test_request_context('/'): - assert flask.url_for('index', _external=True) == 'http://localhost.localdomain:5000/' + self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') with app.test_request_context('/'): - assert flask.url_for('sub', _external=True) == 'http://foo.localhost.localdomain:5000/' + self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') try: with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): pass except Exception, e: assert isinstance(e, ValueError) - assert str(e) == "the server name provided " + \ + self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:5000') does not match the " + \ - "server name from the WSGI environment ('localhost')", str(e) + "server name from the WSGI environment ('localhost')") try: app.config.update(SERVER_NAME='localhost') @@ -679,7 +679,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: rv = app.test_client().get('/') - assert rv.data == 'Foo' + self.assert_equal(rv.data, 'Foo') except ValueError, e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e @@ -687,7 +687,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: rv = app.test_client().get('/', 'http://localhost.localdomain:5000') - assert rv.data == 'Foo' + self.assert_equal(rv.data, 'Foo') except ValueError, e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e @@ -695,7 +695,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: rv = app.test_client().get('/', 'https://localhost.localdomain:5000') - assert rv.data == 'Foo' + self.assert_equal(rv.data, 'Foo') except ValueError, e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e @@ -704,7 +704,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: app.config.update(SERVER_NAME='localhost.localdomain') rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' + self.assert_equal(rv.data, 'Foo') except ValueError, e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e @@ -713,23 +713,23 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: app.config.update(SERVER_NAME='localhost.localdomain:443') rv = app.test_client().get('/', 'https://localhost.localdomain') - assert rv.data == 'Foo' + self.assert_equal(rv.data, 'Foo') except ValueError, e: - assert str(e) == "the server name provided " + \ + self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:443') does not match the " + \ - "server name from the WSGI environment ('localhost.localdomain')", str(e) + "server name from the WSGI environment ('localhost.localdomain')") try: app.config.update(SERVER_NAME='localhost.localdomain') app.test_client().get('/', 'http://foo.localhost') except ValueError, e: - assert str(e) == "the server name provided " + \ + self.assert_equal(str(e), "the server name provided " + \ "('localhost.localdomain') does not match the " + \ - "server name from the WSGI environment ('foo.localhost')", str(e) + "server name from the WSGI environment ('foo.localhost')") try: rv = app.test_client().get('/', 'http://foo.localhost.localdomain') - assert rv.data == 'Foo SubDomain' + self.assert_equal(rv.data, 'Foo SubDomain') except ValueError, e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e @@ -751,7 +751,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): else: self.fail('expected exception') else: - assert c.get('/').status_code == 500 + self.assert_equal(c.get('/').status_code, 500) # we have to run this test in an isolated thread because if the # debug flag is set to true and an exception happens the context is @@ -779,7 +779,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() rv = c.post('/accept', data={'myfile': 'foo' * 100}) - assert rv.data == '42' + self.assert_equal(rv.data, '42') def test_url_processors(self): app = flask.Flask(__name__) @@ -886,9 +886,9 @@ class ContextTestCase(FlaskTestCase): return flask.request.url with app.test_request_context('/?name=World'): - assert index() == 'Hello World!' + self.assert_equal(index(), 'Hello World!') with app.test_request_context('/meh'): - assert meh() == 'http://localhost/meh' + self.assert_equal(meh(), 'http://localhost/meh') assert flask._request_ctx_stack.top is None def test_context_test(self): @@ -911,7 +911,7 @@ class ContextTestCase(FlaskTestCase): ctx = app.test_request_context('/?name=World') ctx.push() - assert index() == 'Hello World!' + self.assert_equal(index(), 'Hello World!') ctx.pop() try: index() @@ -935,10 +935,10 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/', 'http://localhost/') - assert rv.data == 'normal index' + self.assert_equal(rv.data, 'normal index') rv = c.get('/', 'http://test.localhost/') - assert rv.data == 'test index' + self.assert_equal(rv.data, 'test index') @emits_module_deprecation_warning def test_module_static_path_subdomain(self): @@ -948,7 +948,7 @@ class SubdomainTestCase(FlaskTestCase): app.register_module(mod) c = app.test_client() rv = c.get('/static/hello.txt', 'http://foo.example.com/') - assert rv.data.strip() == 'Hello Subdomain' + self.assert_equal(rv.data.strip(), 'Hello Subdomain') def test_subdomain_matching(self): app = flask.Flask(__name__) @@ -959,7 +959,7 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/', 'http://mitsuhiko.localhost/') - assert rv.data == 'index for mitsuhiko' + self.assert_equal(rv.data, 'index for mitsuhiko') @emits_module_deprecation_warning def test_module_subdomain_support(self): @@ -979,9 +979,9 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/test', 'http://testing.localhost/') - assert rv.data == 'Test' + self.assert_equal(rv.data, 'Test') rv = c.get('/outside', 'http://xtesting.localhost/') - assert rv.data == 'Outside' + self.assert_equal(rv.data, 'Outside') def suite(): diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 93122ac5..fdd63fee 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -43,10 +43,10 @@ class ModuleTestCase(FlaskTestCase): return 'the index' app.register_module(admin) c = app.test_client() - assert c.get('/').data == 'the index' - assert c.get('/admin/').data == 'admin index' - assert c.get('/admin/login').data == 'admin login' - assert c.get('/admin/logout').data == 'admin logout' + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(c.get('/admin/').data, 'admin index') + self.assert_equal(c.get('/admin/login').data, 'admin login') + self.assert_equal(c.get('/admin/logout').data, 'admin logout') @emits_module_deprecation_warning def test_default_endpoint_name(self): @@ -57,9 +57,9 @@ class ModuleTestCase(FlaskTestCase): mod.add_url_rule('/', view_func=index) app.register_module(mod) rv = app.test_client().get('/') - assert rv.data == 'Awesome' + self.assert_equal(rv.data, 'Awesome') with app.test_request_context(): - assert flask.url_for('frontend.index') == '/' + self.assert_equal(flask.url_for('frontend.index'), '/') @emits_module_deprecation_warning def test_request_processing(self): @@ -89,13 +89,13 @@ class ModuleTestCase(FlaskTestCase): app.register_module(admin) c = app.test_client() - assert c.get('/').data == 'the index' - assert catched == ['before-app', 'after-app'] + self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(catched, ['before-app', 'after-app']) del catched[:] - assert c.get('/admin/').data == 'the admin' - assert catched == ['before-app', 'before-admin', - 'after-admin', 'after-app'] + self.assert_equal(c.get('/admin/').data, 'the admin') + self.assert_equal(catched, ['before-app', 'before-admin', + 'after-admin', 'after-app']) @emits_module_deprecation_warning def test_context_processors(self): @@ -118,8 +118,8 @@ class ModuleTestCase(FlaskTestCase): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') app.register_module(admin) c = app.test_client() - assert c.get('/').data == '13' - assert c.get('/admin/').data == '123' + self.assert_equal(c.get('/').data, '13') + self.assert_equal(c.get('/admin/').data, '123') @emits_module_deprecation_warning def test_late_binding(self): @@ -129,7 +129,7 @@ class ModuleTestCase(FlaskTestCase): def index(): return '42' app.register_module(admin, url_prefix='/admin') - assert app.test_client().get('/admin/').data == '42' + self.assert_equal(app.test_client().get('/admin/').data, '42') @emits_module_deprecation_warning def test_error_handling(self): @@ -150,11 +150,11 @@ class ModuleTestCase(FlaskTestCase): app.register_module(admin) c = app.test_client() rv = c.get('/') - assert rv.status_code == 404 - assert rv.data == 'not found' + self.assert_equal(rv.status_code, 404) + self.assert_equal(rv.data, 'not found') rv = c.get('/error') - assert rv.status_code == 500 - assert 'internal server error' == rv.data + self.assert_equal(rv.status_code, 500) + self.assert_equal('internal server error', rv.data) def test_templates_and_static(self): app = moduleapp @@ -162,15 +162,15 @@ class ModuleTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') - assert rv.data == 'Hello from the Frontend' + self.assert_equal(rv.data, 'Hello from the Frontend') rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' + self.assert_equal(rv.data, 'Hello from the Admin') rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' + self.assert_equal(rv.data, 'Hello from the Admin') rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' + self.assert_equal(rv.data.strip(), 'Admin File') rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' + self.assert_equal(rv.data.strip(), '/* nested file */') with app.test_request_context(): assert flask.url_for('admin.static', filename='test.txt') \ @@ -180,12 +180,12 @@ class ModuleTestCase(FlaskTestCase): try: flask.render_template('missing.html') except TemplateNotFound, e: - assert e.name == 'missing.html' + self.assert_equal(e.name, 'missing.html') else: assert 0, 'expected exception' with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') def test_safe_access(self): app = moduleapp @@ -245,8 +245,8 @@ class ModuleTestCase(FlaskTestCase): app.register_module(module) c = app.test_client() - assert c.get('/foo/').data == 'index' - assert c.get('/foo/bar').data == 'bar' + self.assert_equal(c.get('/foo/').data, 'index') + self.assert_equal(c.get('/foo/bar').data, 'bar') class BlueprintTestCase(FlaskTestCase): @@ -287,9 +287,9 @@ class BlueprintTestCase(FlaskTestCase): c = app.test_client() - assert c.get('/frontend-no').data == 'frontend says no' - assert c.get('/backend-no').data == 'backend says no' - assert c.get('/what-is-a-sideend').data == 'application itself says no' + self.assert_equal(c.get('/frontend-no').data, 'frontend says no') + self.assert_equal(c.get('/backend-no').data, 'backend says no') + self.assert_equal(c.get('/what-is-a-sideend').data, 'application itself says no') def test_blueprint_url_definitions(self): bp = flask.Blueprint('test', __name__) @@ -344,15 +344,15 @@ class BlueprintTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') - assert rv.data == 'Hello from the Frontend' + self.assert_equal(rv.data, 'Hello from the Frontend') rv = c.get('/admin/') - assert rv.data == 'Hello from the Admin' + self.assert_equal(rv.data, 'Hello from the Admin') rv = c.get('/admin/index2') - assert rv.data == 'Hello from the Admin' + self.assert_equal(rv.data, 'Hello from the Admin') rv = c.get('/admin/static/test.txt') - assert rv.data.strip() == 'Admin File' + self.assert_equal(rv.data.strip(), 'Admin File') rv = c.get('/admin/static/css/test.css') - assert rv.data.strip() == '/* nested file */' + self.assert_equal(rv.data.strip(), '/* nested file */') with app.test_request_context(): assert flask.url_for('admin.static', filename='test.txt') \ @@ -362,12 +362,12 @@ class BlueprintTestCase(FlaskTestCase): try: flask.render_template('missing.html') except TemplateNotFound, e: - assert e.name == 'missing.html' + self.assert_equal(e.name, 'missing.html') else: assert 0, 'expected exception' with flask.Flask(__name__).test_request_context(): - assert flask.render_template('nested/nested.txt') == 'I\'m nested' + self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') def test_templates_list(self): from blueprintapp import app diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index c8bf4687..2b689bc0 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -23,8 +23,8 @@ SECRET_KEY = 'devkey' class ConfigTestCase(FlaskTestCase): def common_object_test(self, app): - assert app.secret_key == 'devkey' - assert app.config['TEST_KEY'] == 'foo' + self.assert_equal(app.secret_key, 'devkey') + self.assert_equal(app.config['TEST_KEY'], 'foo') assert 'ConfigTestCase' not in app.config def test_config_from_file(self): diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py index d691b1dd..531f7f82 100644 --- a/flask/testsuite/deprecations.py +++ b/flask/testsuite/deprecations.py @@ -27,8 +27,8 @@ class DeprecationsTestCase(FlaskTestCase): return app.jinja_env.globals['foo'] c = app.test_client() - assert c.get('/').data == '42' - assert len(log) == 1 + self.assert_equal(c.get('/').data, '42') + self.assert_equal(len(log), 1) assert 'init_jinja_globals' in str(log[0]['message']) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index faea9c8d..50494640 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -47,7 +47,7 @@ class JSONTestCase(FlaskTestCase): c = app.test_client() resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), content_type='application/json; charset=iso-8859-15') - assert resp.data == u'Hällo Wörld'.encode('utf-8') + self.assert_equal(resp.data, u'Hällo Wörld'.encode('utf-8')) def test_jsonify(self): d = dict(a=23, b=42, c=[1, 2, 3]) @@ -61,8 +61,8 @@ class JSONTestCase(FlaskTestCase): c = app.test_client() for url in '/kw', '/dict': rv = c.get(url) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d + self.assert_equal(rv.mimetype, 'application/json') + self.assert_equal(flask.json.loads(rv.data), d) def test_json_attr(self): app = flask.Flask(__name__) @@ -72,16 +72,16 @@ class JSONTestCase(FlaskTestCase): c = app.test_client() rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') - assert rv.data == '3' + self.assert_equal(rv.data, '3') def test_template_escaping(self): app = flask.Flask(__name__) render = flask.render_template_string with app.test_request_context(): rv = render('{{ ""|tojson|safe }}') - assert rv == '"<\\/script>"' + self.assert_equal(rv, '"<\\/script>"') rv = render('{{ "<\0/script>"|tojson|safe }}') - assert rv == '"<\\u0000\\/script>"' + self.assert_equal(rv, '"<\\u0000\\/script>"') def test_modified_url_encoding(self): class ModifiedRequest(flask.Request): @@ -95,8 +95,8 @@ class JSONTestCase(FlaskTestCase): return flask.request.args['foo'] rv = app.test_client().get(u'/?foo=정상처리'.encode('euc-kr')) - assert rv.status_code == 200 - assert rv.data == u'정상처리'.encode('utf-8') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, u'정상처리'.encode('utf-8')) if not has_encoding('euc-kr'): test_modified_url_encoding = None @@ -109,9 +109,9 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): rv = flask.send_file('static/index.html') assert rv.direct_passthrough - assert rv.mimetype == 'text/html' + self.assert_equal(rv.mimetype, 'text/html') with app.open_resource('static/index.html') as f: - assert rv.data == f.read() + self.assert_equal(rv.data, f.read()) def test_send_file_xsendfile(self): app = flask.Flask(__name__) @@ -120,9 +120,9 @@ class SendfileTestCase(FlaskTestCase): rv = flask.send_file('static/index.html') assert rv.direct_passthrough assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') - assert rv.mimetype == 'text/html' + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) + self.assert_equal(rv.mimetype, 'text/html') def test_send_file_object(self): app = flask.Flask(__name__) @@ -131,39 +131,39 @@ class SendfileTestCase(FlaskTestCase): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) with app.open_resource('static/index.html') as f: - assert rv.data == f.read() - assert rv.mimetype == 'text/html' + self.assert_equal(rv.data, f.read()) + self.assert_equal(rv.mimetype, 'text/html') # mimetypes + etag - assert len(captured) == 2 + self.assert_equal(len(captured), 2) app.use_x_sendfile = True with catch_warnings() as captured: with app.test_request_context(): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) - assert rv.mimetype == 'text/html' + self.assert_equal(rv.mimetype, 'text/html') assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') + self.assert_equal(rv.headers['x-sendfile'], + os.path.join(app.root_path, 'static/index.html')) # mimetypes + etag - assert len(captured) == 2 + self.assert_equal(len(captured), 2) app.use_x_sendfile = False with app.test_request_context(): with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f) - assert rv.data == 'Test' - assert rv.mimetype == 'application/octet-stream' + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'application/octet-stream') # etags - assert len(captured) == 1 + self.assert_equal(len(captured), 1) with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f, mimetype='text/plain') - assert rv.data == 'Test' - assert rv.mimetype == 'text/plain' + self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.mimetype, 'text/plain') # etags - assert len(captured) == 1 + self.assert_equal(len(captured), 1) app.use_x_sendfile = True with catch_warnings() as captured: @@ -172,7 +172,7 @@ class SendfileTestCase(FlaskTestCase): rv = flask.send_file(f) assert 'x-sendfile' not in rv.headers # etags - assert len(captured) == 1 + self.assert_equal(len(captured), 1) def test_attachment(self): app = flask.Flask(__name__) @@ -181,25 +181,25 @@ class SendfileTestCase(FlaskTestCase): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f, as_attachment=True) value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' + self.assert_equal(value, 'attachment') # mimetypes + etag - assert len(captured) == 2 + self.assert_equal(len(captured), 2) with app.test_request_context(): - assert options['filename'] == 'index.html' + self.assert_equal(options['filename'], 'index.html') rv = flask.send_file('static/index.html', as_attachment=True) value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.html' + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.html') with app.test_request_context(): rv = flask.send_file(StringIO('Test'), as_attachment=True, attachment_filename='index.txt', add_etags=False) - assert rv.mimetype == 'text/plain' + self.assert_equal(rv.mimetype, 'text/plain') value, options = parse_options_header(rv.headers['Content-Disposition']) - assert value == 'attachment' - assert options['filename'] == 'index.txt' + self.assert_equal(value, 'attachment') + self.assert_equal(options['filename'], 'index.txt') class LoggingTestCase(FlaskTestCase): @@ -208,7 +208,7 @@ class LoggingTestCase(FlaskTestCase): app = flask.Flask(__name__) logger1 = app.logger assert app.logger is logger1 - assert logger1.name == __name__ + self.assert_equal(logger1.name, __name__) app.logger_name = __name__ + '/test_logger_cache' assert app.logger is not logger1 @@ -254,7 +254,7 @@ class LoggingTestCase(FlaskTestCase): 1/0 rv = app.test_client().get('/') - assert rv.status_code == 500 + self.assert_equal(rv.status_code, 500) assert 'Internal Server Error' in rv.data err = out.getvalue() @@ -282,8 +282,8 @@ class LoggingTestCase(FlaskTestCase): return 'Hello Server Error', 500 for trigger in 'before', 'after': rv = app.test_client().get('/') - assert rv.status_code == 500 - assert rv.data == 'Hello Server Error' + self.assert_equal(rv.status_code, 500) + self.assert_equal(rv.data, 'Hello Server Error') def suite(): diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index e55807b3..d9054a47 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -29,10 +29,10 @@ class SignalsTestCase(FlaskTestCase): flask.template_rendered.connect(record, app) try: app.test_client().get('/') - assert len(recorded) == 1 + self.assert_equal(len(recorded), 1) template, context = recorded[0] - assert template.name == 'simple_template.html' - assert context['whiskey'] == 42 + self.assert_equal(template.name, 'simple_template.html') + self.assert_equal(context['whiskey'], 42) finally: flask.template_rendered.disconnect(record, app) @@ -44,7 +44,7 @@ class SignalsTestCase(FlaskTestCase): calls.append('before-signal') def after_request_signal(sender, response): - assert response.data == 'stuff' + self.assert_equal(response.data, 'stuff') calls.append('after-signal') @app.before_request @@ -67,11 +67,11 @@ class SignalsTestCase(FlaskTestCase): try: rv = app.test_client().get('/') - assert rv.data == 'stuff' + self.assert_equal(rv.data, 'stuff') - assert calls == ['before-signal', 'before-handler', + self.assert_equal(calls, ['before-signal', 'before-handler', 'handler', 'after-handler', - 'after-signal'] + 'after-signal']) finally: flask.request_started.disconnect(before_request_signal, app) flask.request_finished.disconnect(after_request_signal, app) @@ -89,8 +89,8 @@ class SignalsTestCase(FlaskTestCase): flask.got_request_exception.connect(record, app) try: - assert app.test_client().get('/').status_code == 500 - assert len(recorded) == 1 + self.assert_equal(app.test_client().get('/').status_code, 500) + self.assert_equal(len(recorded), 1) assert isinstance(recorded[0], ZeroDivisionError) finally: flask.got_request_exception.disconnect(record, app) diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index e980ff92..20d7a16f 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -24,7 +24,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('context_template.html', value=23) rv = app.test_client().get('/') - assert rv.data == '

    23|42' + self.assert_equal(rv.data, '

    23|42') def test_original_win(self): app = flask.Flask(__name__) @@ -32,7 +32,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template_string('{{ config }}', config=42) rv = app.test_client().get('/') - assert rv.data == '42' + self.assert_equal(rv.data, '42') def test_standard_context(self): app = flask.Flask(__name__) @@ -48,7 +48,7 @@ class TemplatingTestCase(FlaskTestCase): {{ session.test }} ''') rv = app.test_client().get('/?foo=42') - assert rv.data.split() == ['42', '23', 'False', 'aha'] + self.assert_equal(rv.data.split(), ['42', '23', 'False', 'aha']) def test_escaping(self): text = '

    Hello World!' @@ -58,14 +58,14 @@ class TemplatingTestCase(FlaskTestCase): return flask.render_template('escaping_template.html', text=text, html=flask.Markup(text)) lines = app.test_client().get('/').data.splitlines() - assert lines == [ + self.assert_equal(lines, [ '<p>Hello World!', '

    Hello World!', '

    Hello World!', '

    Hello World!', '<p>Hello World!', '

    Hello World!' - ] + ]) def test_no_escaping(self): app = flask.Flask(__name__) @@ -79,7 +79,7 @@ class TemplatingTestCase(FlaskTestCase): app = flask.Flask(__name__) with app.test_request_context(): macro = flask.get_template_attribute('_macro.html', 'hello') - assert macro('World') == 'Hello World!' + self.assert_equal(macro('World'), 'Hello World!') def test_template_filter(self): app = flask.Flask(__name__) @@ -87,8 +87,8 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] assert 'my_reverse' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['my_reverse'] == my_reverse - assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') def test_template_filter_with_name(self): app = flask.Flask(__name__) @@ -96,8 +96,8 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] assert 'strrev' in app.jinja_env.filters.keys() - assert app.jinja_env.filters['strrev'] == my_reverse - assert app.jinja_env.filters['strrev']('abcd') == 'dcba' + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') def test_template_filter_with_template(self): app = flask.Flask(__name__) @@ -108,7 +108,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - assert rv.data == 'dcba' + self.assert_equal(rv.data, 'dcba') def test_template_filter_with_name_and_template(self): app = flask.Flask(__name__) @@ -119,7 +119,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - assert rv.data == 'dcba' + self.assert_equal(rv.data, 'dcba') def test_custom_template_loader(self): class MyFlask(flask.Flask): @@ -132,7 +132,7 @@ class TemplatingTestCase(FlaskTestCase): return flask.render_template('index.html') c = app.test_client() rv = c.get('/') - assert rv.data == 'Hello Custom World!' + self.assert_equal(rv.data, 'Hello Custom World!') def suite(): diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 4e16c257..0cf1980d 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -97,14 +97,14 @@ class TestToolsTestCase(FlaskTestCase): with app.test_client() as c: resp = c.get('/') - assert flask.g.value == 42 - assert resp.data == 'Hello World!' - assert resp.status_code == 200 + self.assert_equal(flask.g.value, 42) + self.assert_equal(resp.data, 'Hello World!') + self.assert_equal(resp.status_code, 200) resp = c.get('/other') assert not hasattr(flask.g, 'value') assert 'Internal Server Error' in resp.data - assert resp.status_code == 500 + self.assert_equal(resp.status_code, 500) flask.g.value = 23 try: From fc2caa4b9c75a6ee8569495008efd1f37c27a8cb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:43:27 +0100 Subject: [PATCH 0792/3747] Changed assert to self.assert_ where it was still in place --- flask/testsuite/basic.py | 80 ++++++++++++++++----------------- flask/testsuite/blueprints.py | 18 ++++---- flask/testsuite/config.py | 20 ++++----- flask/testsuite/deprecations.py | 2 +- flask/testsuite/helpers.py | 34 +++++++------- flask/testsuite/signals.py | 2 +- flask/testsuite/templating.py | 12 ++--- flask/testsuite/testing.py | 4 +- 8 files changed, 86 insertions(+), 86 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 8c2be901..d09ec92e 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -73,7 +73,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - assert not rv.data # head truncates + self.assert_(not rv.data) # head truncates self.assert_equal(c.post('/more').data, 'POST') self.assert_equal(c.get('/more').data, 'GET') rv = c.delete('/more') @@ -97,7 +97,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - assert not rv.data # head truncates + self.assert_(not rv.data) # head truncates self.assert_equal(c.post('/more').data, 'POST') self.assert_equal(c.get('/more').data, 'GET') rv = c.delete('/more') @@ -168,8 +168,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) def test_session_using_server_name_and_port(self): app = flask.Flask(__name__) @@ -182,8 +182,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - assert 'domain=.example.com' in rv.headers['set-cookie'].lower() - assert 'httponly' in rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) def test_session_using_application_root(self): class PrefixPathMiddleware(object): @@ -205,7 +205,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - assert 'path=/bar' in rv.headers['set-cookie'].lower() + self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) def test_missing_session(self): app = flask.Flask(__name__) @@ -213,11 +213,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: f(*args, **kwargs) except RuntimeError, e: - assert e.args and 'session is unavailable' in e.args[0] + self.assert_(e.args and 'session is unavailable' in e.args[0]) else: - assert False, 'expected exception' + self.assert_(False, 'expected exception') with app.test_request_context(): - assert flask.session.get('missing_key') is None + self.assert_(flask.session.get('missing_key') is None) expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') @@ -237,7 +237,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): client = app.test_client() rv = client.get('/') - assert 'set-cookie' in rv.headers + self.assert_('set-cookie' in rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime @@ -250,20 +250,20 @@ class BasicFunctionalityTestCase(FlaskTestCase): permanent = False rv = app.test_client().get('/') - assert 'set-cookie' in rv.headers + self.assert_('set-cookie' in rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - assert match is None + self.assert_(match is None) def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' with app.test_request_context(): - assert not flask.session.modified + self.assert_(not flask.session.modified) flask.flash('Zap') flask.session.modified = False flask.flash('Zip') - assert flask.session.modified + self.assert_(flask.session.modified) self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) def test_extended_flashing(self): @@ -308,12 +308,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): return response @app.route('/') def index(): - assert 'before' in evts - assert 'after' not in evts + self.assert_('before' in evts) + self.assert_('after' not in evts) return 'request' - assert 'after' not in evts + self.assert_('after' not in evts) rv = app.test_client().get('/').data - assert 'after' in evts + self.assert_('after' in evts) self.assert_equal(rv, 'request|after') def test_teardown_request_handler(self): @@ -328,7 +328,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - assert 'Response' in rv.data + self.assert_('Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_debug_mode(self): @@ -344,7 +344,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - assert 'Response' in rv.data + self.assert_('Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_error(self): @@ -377,7 +377,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1/0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - assert 'Internal Server Error' in rv.data + self.assert_('Internal Server Error' in rv.data) self.assert_equal(len(called), 2) def test_before_after_request_order(self): @@ -451,7 +451,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.errorhandler(MyException) def handle_my_exception(e): - assert isinstance(e, MyException) + self.assert_(isinstance(e, MyException)) return '42' @app.route('/') def index(): @@ -474,7 +474,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.get('/fail') except KeyError, e: - assert isinstance(e, BadRequest) + self.assert_(isinstance(e, BadRequest)) else: self.fail('Expected exception') @@ -509,8 +509,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.post('/fail', data={'foo': 'index.txt'}) except DebugFilesKeyError, e: - assert 'no file contents were transmitted' in str(e) - assert 'This was submitted: "index.txt"' in str(e) + self.assert_('no file contents were transmitted' in str(e)) + self.assert_('This was submitted: "index.txt"' in str(e)) else: self.fail('Expected exception') @@ -572,8 +572,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): pass with app.test_request_context(): self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x') - assert flask.url_for('hello', name='test x', _external=True) \ - == 'http://localhost/hello/test%20x' + self.assert_equal(flask.url_for('hello', name='test x', _external=True), + 'http://localhost/hello/test%20x') def test_custom_converters(self): from werkzeug.routing import BaseConverter @@ -597,8 +597,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(rv.status_code, 200) self.assert_equal(rv.data.strip(), '

    Hello World!

    ') with app.test_request_context(): - assert flask.url_for('static', filename='index.html') \ - == '/static/index.html' + self.assert_equal(flask.url_for('static', filename='index.html'), + '/static/index.html') def test_none_response(self): app = flask.Flask(__name__) @@ -611,7 +611,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(str(e), 'View function did not return a response') pass else: - assert "Expected ValueError" + self.assert_("Expected ValueError") def test_request_locals(self): self.assert_equal(repr(flask.g), '') @@ -641,7 +641,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): pass except Exception, e: - assert isinstance(e, ValueError) + self.assert_(isinstance(e, ValueError)) self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:5000') does not match the " + \ "server name from the WSGI environment ('localhost')") @@ -768,11 +768,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.before_request def always_first(): flask.request.form['myfile'] - assert False + self.assert_(False) @app.route('/accept', methods=['POST']) def accept_file(): flask.request.form['myfile'] - assert False + self.assert_(False) @app.errorhandler(413) def catcher(error): return '42' @@ -889,17 +889,17 @@ class ContextTestCase(FlaskTestCase): self.assert_equal(index(), 'Hello World!') with app.test_request_context('/meh'): self.assert_equal(meh(), 'http://localhost/meh') - assert flask._request_ctx_stack.top is None + self.assert_(flask._request_ctx_stack.top is None) def test_context_test(self): app = flask.Flask(__name__) - assert not flask.request - assert not flask.has_request_context() + self.assert_(not flask.request) + self.assert_(not flask.has_request_context()) ctx = app.test_request_context() ctx.push() try: - assert flask.request - assert flask.has_request_context() + self.assert_(flask.request) + self.assert_(flask.has_request_context()) finally: ctx.pop() @@ -918,7 +918,7 @@ class ContextTestCase(FlaskTestCase): except RuntimeError: pass else: - assert 0, 'expected runtime error' + self.assert_(0, 'expected runtime error') class SubdomainTestCase(FlaskTestCase): diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index fdd63fee..88e2be36 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -173,8 +173,8 @@ class ModuleTestCase(FlaskTestCase): self.assert_equal(rv.data.strip(), '/* nested file */') with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') with app.test_request_context(): try: @@ -182,7 +182,7 @@ class ModuleTestCase(FlaskTestCase): except TemplateNotFound, e: self.assert_equal(e.name, 'missing.html') else: - assert 0, 'expected exception' + self.assert_(0, 'expected exception') with flask.Flask(__name__).test_request_context(): self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') @@ -198,13 +198,13 @@ class ModuleTestCase(FlaskTestCase): except NotFound: pass else: - assert 0, 'expected exception' + self.assert_(0, 'expected exception') try: f('../__init__.py') except NotFound: pass else: - assert 0, 'expected exception' + self.assert_(0, 'expected exception') # testcase for a security issue that may exist on windows systems import os @@ -217,7 +217,7 @@ class ModuleTestCase(FlaskTestCase): except NotFound: pass else: - assert 0, 'expected exception' + self.assert_(0, 'expected exception') finally: os.path = old_path @@ -355,8 +355,8 @@ class BlueprintTestCase(FlaskTestCase): self.assert_equal(rv.data.strip(), '/* nested file */') with app.test_request_context(): - assert flask.url_for('admin.static', filename='test.txt') \ - == '/admin/static/test.txt' + self.assert_equal(flask.url_for('admin.static', filename='test.txt'), + '/admin/static/test.txt') with app.test_request_context(): try: @@ -364,7 +364,7 @@ class BlueprintTestCase(FlaskTestCase): except TemplateNotFound, e: self.assert_equal(e.name, 'missing.html') else: - assert 0, 'expected exception' + self.assert_(0, 'expected exception') with flask.Flask(__name__).test_request_context(): self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 2b689bc0..b9fa6b48 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -25,7 +25,7 @@ class ConfigTestCase(FlaskTestCase): def common_object_test(self, app): self.assert_equal(app.secret_key, 'devkey') self.assert_equal(app.config['TEST_KEY'], 'foo') - assert 'ConfigTestCase' not in app.config + self.assert_('ConfigTestCase' not in app.config) def test_config_from_file(self): app = flask.Flask(__name__) @@ -54,13 +54,13 @@ class ConfigTestCase(FlaskTestCase): try: app.config.from_envvar('FOO_SETTINGS') except RuntimeError, e: - assert "'FOO_SETTINGS' is not set" in str(e) + self.assert_("'FOO_SETTINGS' is not set" in str(e)) else: - assert 0, 'expected exception' - assert not app.config.from_envvar('FOO_SETTINGS', silent=True) + self.assert_(0, 'expected exception') + self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) os.environ = {'FOO_SETTINGS': __file__.rsplit('.')[0] + '.py'} - assert app.config.from_envvar('FOO_SETTINGS') + self.assert_(app.config.from_envvar('FOO_SETTINGS')) self.common_object_test(app) finally: os.environ = env @@ -71,12 +71,12 @@ class ConfigTestCase(FlaskTestCase): app.config.from_pyfile('missing.cfg') except IOError, e: msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.cfg'") + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) else: - assert 0, 'expected config' - assert not app.config.from_pyfile('missing.cfg', silent=True) + self.assert_(0, 'expected config') + self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) class InstanceTestCase(FlaskTestCase): diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py index 531f7f82..062f40b0 100644 --- a/flask/testsuite/deprecations.py +++ b/flask/testsuite/deprecations.py @@ -29,7 +29,7 @@ class DeprecationsTestCase(FlaskTestCase): c = app.test_client() self.assert_equal(c.get('/').data, '42') self.assert_equal(len(log), 1) - assert 'init_jinja_globals' in str(log[0]['message']) + self.assert_('init_jinja_globals' in str(log[0]['message'])) def suite(): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 50494640..4bd05e3d 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -108,7 +108,7 @@ class SendfileTestCase(FlaskTestCase): app = flask.Flask(__name__) with app.test_request_context(): rv = flask.send_file('static/index.html') - assert rv.direct_passthrough + self.assert_(rv.direct_passthrough) self.assert_equal(rv.mimetype, 'text/html') with app.open_resource('static/index.html') as f: self.assert_equal(rv.data, f.read()) @@ -118,8 +118,8 @@ class SendfileTestCase(FlaskTestCase): app.use_x_sendfile = True with app.test_request_context(): rv = flask.send_file('static/index.html') - assert rv.direct_passthrough - assert 'x-sendfile' in rv.headers + self.assert_(rv.direct_passthrough) + self.assert_('x-sendfile' in rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) self.assert_equal(rv.mimetype, 'text/html') @@ -142,7 +142,7 @@ class SendfileTestCase(FlaskTestCase): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) self.assert_equal(rv.mimetype, 'text/html') - assert 'x-sendfile' in rv.headers + self.assert_('x-sendfile' in rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) # mimetypes + etag @@ -170,7 +170,7 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): f = StringIO('Test') rv = flask.send_file(f) - assert 'x-sendfile' not in rv.headers + self.assert_('x-sendfile' not in rv.headers) # etags self.assert_equal(len(captured), 1) @@ -207,10 +207,10 @@ class LoggingTestCase(FlaskTestCase): def test_logger_cache(self): app = flask.Flask(__name__) logger1 = app.logger - assert app.logger is logger1 + self.assert_(app.logger is logger1) self.assert_equal(logger1.name, __name__) app.logger_name = __name__ + '/test_logger_cache' - assert app.logger is not logger1 + self.assert_(app.logger is not logger1) def test_debug_log(self): app = flask.Flask(__name__) @@ -230,10 +230,10 @@ class LoggingTestCase(FlaskTestCase): with catch_stderr() as err: c.get('/') out = err.getvalue() - assert 'WARNING in helpers [' in out - assert os.path.basename(__file__.rsplit('.')[0] + '.py') in out - assert 'the standard library is dead' in out - assert 'this is a debug statement' in out + self.assert_('WARNING in helpers [' in out) + self.assert_(os.path.basename(__file__.rsplit('.')[0] + '.py') in out) + self.assert_('the standard library is dead' in out) + self.assert_('this is a debug statement' in out) with catch_stderr() as err: try: @@ -241,7 +241,7 @@ class LoggingTestCase(FlaskTestCase): except ZeroDivisionError: pass else: - assert False, 'debug log ate the exception' + self.assert_(False, 'debug log ate the exception') def test_exception_logging(self): out = StringIO() @@ -255,13 +255,13 @@ class LoggingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - assert 'Internal Server Error' in rv.data + self.assert_('Internal Server Error' in rv.data) err = out.getvalue() - assert 'Exception on / [GET]' in err - assert 'Traceback (most recent call last):' in err - assert '1/0' in err - assert 'ZeroDivisionError:' in err + self.assert_('Exception on / [GET]' in err) + self.assert_('Traceback (most recent call last):' in err) + self.assert_('1/0' in err) + self.assert_('ZeroDivisionError:' in err) def test_processor_exceptions(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index d9054a47..da1a68ca 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -91,7 +91,7 @@ class SignalsTestCase(FlaskTestCase): try: self.assert_equal(app.test_client().get('/').status_code, 500) self.assert_equal(len(recorded), 1) - assert isinstance(recorded[0], ZeroDivisionError) + self.assert_(isinstance(recorded[0], ZeroDivisionError)) finally: flask.got_request_exception.disconnect(record, app) diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 20d7a16f..eadbdcf6 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -70,10 +70,10 @@ class TemplatingTestCase(FlaskTestCase): def test_no_escaping(self): app = flask.Flask(__name__) with app.test_request_context(): - assert flask.render_template_string('{{ foo }}', - foo='') == '' - assert flask.render_template('mail.txt', foo='') \ - == ' Mail' + self.assert_equal(flask.render_template_string('{{ foo }}', + foo=''), '') + self.assert_equal(flask.render_template('mail.txt', foo=''), + ' Mail') def test_macros(self): app = flask.Flask(__name__) @@ -86,7 +86,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter() def my_reverse(s): return s[::-1] - assert 'my_reverse' in app.jinja_env.filters.keys() + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -95,7 +95,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter('strrev') def my_reverse(s): return s[::-1] - assert 'strrev' in app.jinja_env.filters.keys() + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 0cf1980d..f665e12c 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -102,8 +102,8 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(resp.status_code, 200) resp = c.get('/other') - assert not hasattr(flask.g, 'value') - assert 'Internal Server Error' in resp.data + self.assert_(not hasattr(flask.g, 'value')) + self.assert_('Internal Server Error' in resp.data) self.assert_equal(resp.status_code, 500) flask.g.value = 23 From d5cd4f8d59a4a8e5860c8813b3f5d3f3631e9183 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:48:09 +0100 Subject: [PATCH 0793/3747] Updated manifests --- MANIFEST.in | 5 ++++- setup.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 3fef8b5b..f82ed054 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include Makefile CHANGES LICENSE AUTHORS +include Makefile CHANGES LICENSE AUTHORS run-tests.py recursive-include artwork * recursive-include tests * recursive-include examples * @@ -9,5 +9,8 @@ recursive-exclude tests *.pyc recursive-exclude tests *.pyo recursive-exclude examples *.pyc recursive-exclude examples *.pyo +recursive-include flask/testsuite/static * +recursive-include flask/testsuite/templates * +recursive-include flask/testsuite/test_apps * prune docs/_build prune docs/_themes/.git diff --git a/setup.py b/setup.py index a51c3887..db2eb9c7 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,10 @@ setup( description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - packages=['flask'], + packages=['flask', 'flask.testsuite'], + package_data={ + 'flask.testsuite': ['test_apps/*', 'static/*', 'templates/*'] + }, zip_safe=False, platforms='any', install_requires=[ From fbd6776e68a12aa7bf7d646ca03d568cedc616f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:48:33 +0100 Subject: [PATCH 0794/3747] Fixed a bug in the testsuite that caused problems when dots where in directory names --- flask/testsuite/config.py | 4 ++-- flask/testsuite/helpers.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index b9fa6b48..2ed726c8 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -29,7 +29,7 @@ class ConfigTestCase(FlaskTestCase): def test_config_from_file(self): app = flask.Flask(__name__) - app.config.from_pyfile(__file__.rsplit('.')[0] + '.py') + app.config.from_pyfile(__file__.rsplit('.', 1)[0] + '.py') self.common_object_test(app) def test_config_from_object(self): @@ -59,7 +59,7 @@ class ConfigTestCase(FlaskTestCase): self.assert_(0, 'expected exception') self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) - os.environ = {'FOO_SETTINGS': __file__.rsplit('.')[0] + '.py'} + os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} self.assert_(app.config.from_envvar('FOO_SETTINGS')) self.common_object_test(app) finally: diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 4bd05e3d..56264f70 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -231,7 +231,7 @@ class LoggingTestCase(FlaskTestCase): c.get('/') out = err.getvalue() self.assert_('WARNING in helpers [' in out) - self.assert_(os.path.basename(__file__.rsplit('.')[0] + '.py') in out) + self.assert_(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) self.assert_('the standard library is dead' in out) self.assert_('this is a debug statement' in out) From 0851d956b39634d2cd1618e8c075149d85fb599f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:55:11 +0100 Subject: [PATCH 0795/3747] Updated README --- README | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/README b/README index 5c0eb4f1..7d5ada23 100644 --- a/README +++ b/README @@ -11,24 +11,41 @@ ~ Is it ready? - A preview release is out now, and I'm hoping for some - input about what you want from a microframework and - how it should look like. Consider the API to slightly - improve over time. + It's still not 1.0 but it's shaping up nicely and is + already widely used. Consider the API to slightly + improve over time but we don't plan on breaking it. ~ What do I need? - Jinja 2.4 and Werkzeug 0.6.1. `easy_install` will - install them for you if you do `easy_install Flask==dev`. + Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will + install them for you if you do `easy_install Flask`. I encourage you to use a virtualenv. Check the docs for complete installation and usage instructions. ~ Where are the docs? - Go to http://flask.pocoo.org/ for a prebuilt version of - the current documentation. Otherwise build them yourself + Go to http://flask.pocoo.org/docs/ for a prebuilt version + of the current documentation. Otherwise build them yourself from the sphinx sources in the docs folder. + ~ Where are the tests? + + Good that you're asking. The tests are in the + flask/testsuite package. To run the tests use the + `run-tests.py` file: + + $ python run-tests.py + + If it's not enough output for you, you can use the + `--verbose` flag: + + $ python run-tests.py --verbose + + If you just want one particular testcase to run you can + provide it on the command line: + + $ python run-tests.py test_to_run + ~ Where can I get help? Either use the #pocoo IRC channel on irc.freenode.net or From 29b7efa36bd12234a4086cafbe9b8a4099991d08 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 11:59:52 +0100 Subject: [PATCH 0796/3747] Improved the logic in which tests are found --- run-tests.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/run-tests.py b/run-tests.py index 96db7f9a..176094b6 100644 --- a/run-tests.py +++ b/run-tests.py @@ -37,21 +37,21 @@ class BetterLoader(TestLoader): if testname[len(common_prefix):] == name: return testcase - all_results = [] + all_tests = [] for testcase, testname in find_all_tests_with_name(): - if testname.endswith('.' + name): - all_results.append((testcase, testname)) + if testname.endswith('.' + name) or ('.' + name + '.') in testname: + all_tests.append(testcase) - if len(all_results) == 1: - return all_results[0][0] - elif not len(all_results): - error = 'could not find testcase "%s"' % name - else: - error = 'Too many matches: for "%s"\n%s' % \ - (name, '\n'.join(' - ' + n for c, n in all_results)) + if not all_tests: + print >> sys.stderr, 'Error: could not find test case for "%s"' % name + sys.exit(1) - print >> sys.stderr, 'Error: %s' % error - sys.exit(1) + if len(all_tests) == 1: + return all_tests[0] + rv = unittest.TestSuite() + for test in all_tests: + rv.addTest(test) + return rv unittest.main(testLoader=BetterLoader(), defaultTest='suite') From f30b1174b85ae44b4d215045ef3361f2c3b3a367 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 12:00:26 +0100 Subject: [PATCH 0797/3747] Also support full qualified test names --- run-tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run-tests.py b/run-tests.py index 176094b6..b74e7f71 100644 --- a/run-tests.py +++ b/run-tests.py @@ -39,7 +39,8 @@ class BetterLoader(TestLoader): all_tests = [] for testcase, testname in find_all_tests_with_name(): - if testname.endswith('.' + name) or ('.' + name + '.') in testname: + if testname.endswith('.' + name) or ('.' + name + '.') in testname or \ + testname.startswith(name + '.'): all_tests.append(testcase) if not all_tests: From 5a496885544989ca702e2c9996372a8ebf2cf00d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 12:02:40 +0100 Subject: [PATCH 0798/3747] Moved loader code into the testsuite and out of the runner --- flask/testsuite/__init__.py | 52 +++++++++++++++++++++++++++++++++ run-tests.py | 57 +------------------------------------ 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 8df7a7fd..ff0224bc 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -20,6 +20,9 @@ from contextlib import contextmanager from werkzeug.utils import import_string, find_modules +common_prefix = __name__ + '.' + + def add_to_path(path): def _samefile(x, y): try: @@ -47,6 +50,25 @@ def iter_suites(): yield mod.suite() +def find_all_tests(): + suites = [suite()] + while suites: + s = suites.pop() + try: + suites.extend(s) + except TypeError: + yield s + + +def find_all_tests_with_name(): + for testcase in find_all_tests(): + yield testcase, '%s.%s.%s' % ( + testcase.__class__.__module__, + testcase.__class__.__name__, + testcase._testMethodName + ) + + @contextmanager def catch_warnings(): """Catch warnings in a with block in a list""" @@ -113,6 +135,36 @@ class FlaskTestCase(unittest.TestCase): return self.assertEqual(x, y) +class BetterLoader(unittest.TestLoader): + + def loadTestsFromName(self, name, module=None): + if name == 'suite': + return suite() + for testcase, testname in find_all_tests_with_name(): + if testname == name: + return testcase + if testname.startswith(common_prefix): + if testname[len(common_prefix):] == name: + return testcase + + all_tests = [] + for testcase, testname in find_all_tests_with_name(): + if testname.endswith('.' + name) or ('.' + name + '.') in testname or \ + testname.startswith(name + '.'): + all_tests.append(testcase) + + if not all_tests: + print >> sys.stderr, 'Error: could not find test case for "%s"' % name + sys.exit(1) + + if len(all_tests) == 1: + return all_tests[0] + rv = unittest.TestSuite() + for test in all_tests: + rv.addTest(test) + return rv + + def suite(): setup_paths() suite = unittest.TestSuite() diff --git a/run-tests.py b/run-tests.py index b74e7f71..7d44febc 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,58 +1,3 @@ -import sys import unittest -from unittest.loader import TestLoader -from flask.testsuite import suite - -common_prefix = suite.__module__ + '.' - - -def find_all_tests(): - suites = [suite()] - while suites: - s = suites.pop() - try: - suites.extend(s) - except TypeError: - yield s - - -def find_all_tests_with_name(): - for testcase in find_all_tests(): - yield testcase, '%s.%s.%s' % ( - testcase.__class__.__module__, - testcase.__class__.__name__, - testcase._testMethodName - ) - - -class BetterLoader(TestLoader): - - def loadTestsFromName(self, name, module=None): - if name == 'suite': - return suite() - for testcase, testname in find_all_tests_with_name(): - if testname == name: - return testcase - if testname.startswith(common_prefix): - if testname[len(common_prefix):] == name: - return testcase - - all_tests = [] - for testcase, testname in find_all_tests_with_name(): - if testname.endswith('.' + name) or ('.' + name + '.') in testname or \ - testname.startswith(name + '.'): - all_tests.append(testcase) - - if not all_tests: - print >> sys.stderr, 'Error: could not find test case for "%s"' % name - sys.exit(1) - - if len(all_tests) == 1: - return all_tests[0] - rv = unittest.TestSuite() - for test in all_tests: - rv.addTest(test) - return rv - - +from flask.testsuite import BetterLoader unittest.main(testLoader=BetterLoader(), defaultTest='suite') From a082a5e0ba81af15653fe56501c8d2530d3621dc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 12:07:49 +0100 Subject: [PATCH 0799/3747] Cleanup in the test finder --- flask/testsuite/__init__.py | 51 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index ff0224bc..5ebc786e 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -20,9 +20,6 @@ from contextlib import contextmanager from werkzeug.utils import import_string, find_modules -common_prefix = __name__ + '.' - - def add_to_path(path): def _samefile(x, y): try: @@ -50,23 +47,18 @@ def iter_suites(): yield mod.suite() -def find_all_tests(): - suites = [suite()] +def find_all_tests(suite): + suites = [suite] while suites: s = suites.pop() try: suites.extend(s) except TypeError: - yield s - - -def find_all_tests_with_name(): - for testcase in find_all_tests(): - yield testcase, '%s.%s.%s' % ( - testcase.__class__.__module__, - testcase.__class__.__name__, - testcase._testMethodName - ) + yield s, '%s.%s.%s' % ( + s.__class__.__module__, + s.__class__.__name__, + s._testMethodName + ) @contextmanager @@ -111,6 +103,10 @@ def emits_module_deprecation_warning(f): class FlaskTestCase(unittest.TestCase): + """Baseclass for all the tests that Flask uses. Use these methods + for testing instead of the camelcased ones in the baseclass for + consistency. + """ def ensure_clean_request_context(self): # make sure we're not leaking a request context since we are @@ -136,20 +132,27 @@ class FlaskTestCase(unittest.TestCase): class BetterLoader(unittest.TestLoader): + """A nicer loader that solves two problems. First of all we are setting + up tests from different sources and we're doing this programmatically + which breaks the default loading logic so this is required anyways. + Secondly this loader has a nicer interpolation for test names than the + default one so you can just do ``run-tests.py ViewTestCase`` and it + will work. + """ + + def getRootSuite(self): + return suite() def loadTestsFromName(self, name, module=None): + root = self.getRootSuite() if name == 'suite': - return suite() - for testcase, testname in find_all_tests_with_name(): - if testname == name: - return testcase - if testname.startswith(common_prefix): - if testname[len(common_prefix):] == name: - return testcase + return root all_tests = [] - for testcase, testname in find_all_tests_with_name(): - if testname.endswith('.' + name) or ('.' + name + '.') in testname or \ + for testcase, testname in find_all_tests(root): + if testname == name or \ + testname.endswith('.' + name) or \ + ('.' + name + '.') in testname or \ testname.startswith(name + '.'): all_tests.append(testcase) From 5235c3e37e17b3271e3cac4646eaf97fd1cc071a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 12:09:55 +0100 Subject: [PATCH 0800/3747] Make BetterLoader() have a better api :) --- flask/testsuite/__init__.py | 3 +-- run-tests.py | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 5ebc786e..addbb92b 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -157,8 +157,7 @@ class BetterLoader(unittest.TestLoader): all_tests.append(testcase) if not all_tests: - print >> sys.stderr, 'Error: could not find test case for "%s"' % name - sys.exit(1) + raise LookupError('could not find test case for "%s"' % name) if len(all_tests) == 1: return all_tests[0] diff --git a/run-tests.py b/run-tests.py index 7d44febc..0c8d0bdf 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,3 +1,6 @@ import unittest from flask.testsuite import BetterLoader -unittest.main(testLoader=BetterLoader(), defaultTest='suite') +try: + unittest.main(testLoader=BetterLoader(), defaultTest='suite') +except Exception, e: + print 'Error: %s' % e From c8ec453d860ae4754331170108ef03322f29889b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 13:47:01 +0100 Subject: [PATCH 0801/3747] Require that cookies are enabled in the test client for session transactions --- flask/testing.py | 11 ++++++----- flask/testsuite/testing.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/flask/testing.py b/flask/testing.py index c4d18ca2..6ce96163 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -53,10 +53,12 @@ class FlaskClient(Client): :meth:`~flask.Flask.test_request_context` which are directly passed through. """ + if self.cookie_jar is None: + raise RuntimeError('Session transactions only make sense ' + 'with cookies enabled.') app = self.application environ_overrides = kwargs.pop('environ_overrides', {}) - if self.cookie_jar is not None: - self.cookie_jar.inject_wsgi(environ_overrides) + self.cookie_jar.inject_wsgi(environ_overrides) outer_reqctx = _request_ctx_stack.top with app.test_request_context(*args, **kwargs) as c: sess = app.open_session(c.request) @@ -80,9 +82,8 @@ class FlaskClient(Client): resp = app.response_class() if not app.session_interface.is_null_session(sess): app.save_session(sess, resp) - if self.cookie_jar is not None: - headers = resp.get_wsgi_headers(c.request.environ) - self.cookie_jar.extract_wsgi(c.request.environ, headers) + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) def open(self, *args, **kwargs): if self.context_preserved: diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index f665e12c..b7a71b1a 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -84,6 +84,18 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction(): self.assert_(req is flask.request._get_current_object()) + def test_session_transaction_needs_cookies(self): + app = flask.Flask(__name__) + app.testing = True + c = app.test_client(use_cookies=False) + try: + with c.session_transaction() as s: + pass + except RuntimeError, e: + self.assert_('cookies' in str(e)) + else: + self.fail('Expected runtime error') + def test_test_client_context_binding(self): app = flask.Flask(__name__) @app.route('/') From d49221bf2eeabbfa4a5be4e537b35bae1eb6d272 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 14:01:46 +0100 Subject: [PATCH 0802/3747] The test client now properly pops response contexts on __exit__ --- CHANGES | 2 ++ docs/upgrading.rst | 5 +++++ flask/testing.py | 13 +++++++++---- flask/testsuite/testing.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index fa8c00bf..b39a2001 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,8 @@ Relase date to be decided, codename to be chosen. as defaults. - Added :attr:`flask.views.View.decorators` to support simpler decorating of pluggable (class based) views. +- Fixed an issue where the test client if used with the with statement did not + trigger the execution of the teardown handlers. Version 0.7.3 ------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index b318292c..0ba46c13 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -36,6 +36,11 @@ longer have to handle that error to avoid an internal server error showing up for the user. If you were catching this down explicitly in the past as `ValueError` you will need to change this. +Due to a bug in the test client Flask 0.7 did not trigger teardown +handlers when the test client was used in a with statement. This was +since fixed but might require some changes in your testsuites if you +relied on this behavior. + Version 0.7 ----------- diff --git a/flask/testing.py b/flask/testing.py index 6ce96163..612b4d4d 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -86,9 +86,7 @@ class FlaskClient(Client): self.cookie_jar.extract_wsgi(c.request.environ, headers) def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False + self._pop_reqctx_if_necessary() kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context @@ -114,5 +112,12 @@ class FlaskClient(Client): def __exit__(self, exc_type, exc_value, tb): self.preserve_context = False + self._pop_reqctx_if_necessary() + + def _pop_reqctx_if_necessary(self): if self.context_preserved: - _request_ctx_stack.pop() + # we have to use _request_ctx_stack.top.pop instead of + # _request_ctx_stack.pop since we want teardown handlers + # to be executed. + _request_ctx_stack.top.pop() + self.context_preserved = False diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index b7a71b1a..32867c3f 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -126,6 +126,38 @@ class TestToolsTestCase(FlaskTestCase): else: raise AssertionError('some kind of exception expected') + def test_reuse_client(self): + app = flask.Flask(__name__) + c = app.test_client() + + with c: + self.assert_equal(c.get('/').status_code, 404) + + with c: + self.assert_equal(c.get('/').status_code, 404) + + def test_test_client_calls_teardown_handlers(self): + app = flask.Flask(__name__) + called = [] + @app.teardown_request + def remember(error): + called.append(error) + + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + self.assert_equal(called, [None]) + + del called[:] + with app.test_client() as c: + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, []) + c.get('/') + self.assert_equal(called, [None]) + self.assert_equal(called, [None, None]) + def suite(): suite = unittest.TestSuite() From b256e9f36c6d170e7209a577efcac2503c8e48e3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 14:05:08 +0100 Subject: [PATCH 0803/3747] make_default_options_response now tries to use Werkzeug 0.7 functionality before falling back. --- flask/app.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/flask/app.py b/flask/app.py index b4ae64a5..667f5225 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1305,17 +1305,18 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - # This would be nicer in Werkzeug 0.7, which however currently - # is not released. Werkzeug 0.7 provides a method called - # allowed_methods() that returns all methods that are valid for - # a given path. - methods = [] - try: - _request_ctx_stack.top.url_adapter.match(method='--') - except MethodNotAllowed, e: - methods = e.valid_methods - except HTTPException, e: - pass + adapter = _request_ctx_stack.top.url_adapter + if hasattr(adapter, 'allowed_methods'): + methods = adapter.allowed_methods() + else: + # fallback for Werkzeug < 0.7 + methods = [] + try: + adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass rv = self.response_class() rv.allow.update(methods) return rv From 67101c8b9331d289b3ae5428127814d0c19d643c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 14:41:25 +0100 Subject: [PATCH 0804/3747] Fake signals no better follow the blinker api --- flask/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/signals.py b/flask/signals.py index 4eedf68f..984accb7 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -34,7 +34,7 @@ except ImportError: 'not installed.') send = lambda *a, **kw: None connect = disconnect = has_receivers_for = receivers_for = \ - temporarily_connected_to = _fail + temporarily_connected_to = connected_to = _fail del _fail # the namespace for code signals. If you are not flask code, do From 367b254c787c59b208899f1dfee78904c1784c73 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 14:51:28 +0100 Subject: [PATCH 0805/3747] Make sure that there is a test for subdomain matching with ports --- flask/testsuite/basic.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index d09ec92e..947d0073 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -961,6 +961,17 @@ class SubdomainTestCase(FlaskTestCase): rv = c.get('/', 'http://mitsuhiko.localhost/') self.assert_equal(rv.data, 'index for mitsuhiko') + def test_subdomain_matching_with_ports(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost:3000' + @app.route('/', subdomain='') + def index(user): + return 'index for %s' % user + + c = app.test_client() + rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + self.assert_equal(rv.data, 'index for mitsuhiko') + @emits_module_deprecation_warning def test_module_subdomain_support(self): app = flask.Flask(__name__) From e509d25d32965a8afd7ca98d67ee6f5a6af11cc8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 17:35:47 +0100 Subject: [PATCH 0806/3747] Some more cleanups in how the test runner is invoked --- flask/testsuite/__init__.py | 31 +++++++++++++++++++++++++------ run-tests.py | 8 ++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index addbb92b..3f807251 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -21,6 +21,10 @@ from werkzeug.utils import import_string, find_modules def add_to_path(path): + """Adds an entry to sys.path_info if it's not already there.""" + if not os.path.isdir(path): + raise RuntimeError('Tried to add nonexisting path') + def _samefile(x, y): try: return os.path.samefile(x, y) @@ -35,12 +39,8 @@ def add_to_path(path): sys.path.append(path) -def setup_paths(): - add_to_path(os.path.abspath(os.path.join( - os.path.dirname(__file__), 'test_apps'))) - - def iter_suites(): + """Yields all testsuites.""" for module in find_modules(__name__): mod = import_string(module) if hasattr(mod, 'suite'): @@ -48,6 +48,7 @@ def iter_suites(): def find_all_tests(suite): + """Yields all the tests and their names from a given suite.""" suites = [suite] while suites: s = suites.pop() @@ -167,9 +168,27 @@ class BetterLoader(unittest.TestLoader): return rv +def setup_path(): + add_to_path(os.path.abspath(os.path.join( + os.path.dirname(__file__), 'test_apps'))) + + def suite(): - setup_paths() + """A testsuite that has all the Flask tests. You can use this + function to integrate the Flask tests into your own testsuite + in case you want to test that monkeypatches to Flask do not + break it. + """ + setup_path() suite = unittest.TestSuite() for other_suite in iter_suites(): suite.addTest(other_suite) return suite + + +def main(): + """Runs the testsuite as command line application.""" + try: + unittest.main(testLoader=BetterLoader(), defaultTest='suite') + except Exception, e: + print 'Error: %s' % e diff --git a/run-tests.py b/run-tests.py index 0c8d0bdf..3f9259c4 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,6 +1,2 @@ -import unittest -from flask.testsuite import BetterLoader -try: - unittest.main(testLoader=BetterLoader(), defaultTest='suite') -except Exception, e: - print 'Error: %s' % e +from flask.testsuite import main +main() From c0f42a0978d804237d7bd540f81dcdbc1643ea7f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 18:20:33 +0100 Subject: [PATCH 0807/3747] Added shebang to the test run file --- run-tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/run-tests.py b/run-tests.py index 3f9259c4..4ef8a72d 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,2 +1,3 @@ +#!/usr/bin/env python from flask.testsuite import main main() From 2e4c39199d3c8cdcf459675cba6e04bd30f98120 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 21:30:41 +0200 Subject: [PATCH 0808/3747] Refactored logging of internal server errors. Can now be customized --- flask/app.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/flask/app.py b/flask/app.py index 667f5225..1136469f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1216,14 +1216,24 @@ class Flask(_PackageBoundObject): else: raise e - self.logger.exception('Exception on %s [%s]' % ( - request.path, - request.method - )) + self.log_exception((exc_type, exc_value, tb)) if handler is None: return InternalServerError() return handler(e) + def log_exception(self, exc_info): + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error('Exception on %s [%s]' % ( + request.path, + request.method + ), exc_info=exc_info) + def raise_routing_exception(self, request): """Exceptions that are recording during routing are reraised with this method. During debug we are not reraising redirect requests From dc05722b363b4e77357fb943b2d1ee8abea94cb4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 26 Aug 2011 21:42:17 +0200 Subject: [PATCH 0809/3747] Made the foreword less defensive. --- docs/foreword.rst | 50 ++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 616c298b..10b886bf 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -9,27 +9,30 @@ What does "micro" mean? ----------------------- To me, the "micro" in microframework refers not only to the simplicity and -small size of the framework, but also to the typically limited complexity -and size of applications that are written with the framework. Also the -fact that you can have an entire application in a single Python file. To -be approachable and concise, a microframework sacrifices a few features -that may be necessary in larger or more complex applications. +small size of the framework, but also the fact that it does not make much +decisions for you. While Flask does pick a templating engine for you, we +won't make such decisions for your datastore or other parts. -For example, Flask uses thread-local objects internally so that you don't -have to pass objects around from function to function within a request in -order to stay threadsafe. While this is a really easy approach and saves -you a lot of time, it might also cause some troubles for very large -applications because changes on these thread-local objects can happen -anywhere in the same thread. +For us however the term “micro” does not mean that the whole implementation +has to fit into a single Python file. -Flask provides some tools to deal with the downsides of this approach but -it might be an issue for larger applications because in theory -modifications on these objects might happen anywhere in the same thread. +One of the design decisions with Flask was that simple tasks should be +simple and not take up a lot of code and yet not limit yourself. Because +of that we took a few design choices that some people might find +surprising or unorthodox. For example, Flask uses thread-local objects +internally so that you don't have to pass objects around from function to +function within a request in order to stay threadsafe. While this is a +really easy approach and saves you a lot of time, it might also cause some +troubles for very large applications because changes on these thread-local +objects can happen anywhere in the same thread. In order to solve these +problems we don't hide the thread locals for you but instead embrace them +and provide you with a lot of tools to make it as pleasant as possible to +work with them. Flask is also based on convention over configuration, which means that many things are preconfigured. For example, by convention, templates and static files are in subdirectories within the Python source tree of the -application. +application. While this can be changed you usually don't have to. The main reason however why Flask is called a "microframework" is the idea to keep the core simple but extensible. There is no database abstraction @@ -40,22 +43,15 @@ was implemented in Flask itself. There are currently extensions for object relational mappers, form validation, upload handling, various open authentication technologies and more. -However Flask is not much code and it is built on a very solid foundation -and with that it is very easy to adapt for large applications. If you are -interested in that, check out the :ref:`becomingbig` chapter. +Since Flask is based on a very solid foundation there is not a lot of code +in Flask itself. As such it's easy to adapt even for lage applications +and we are making sure that you can either configure it as much as +possible by subclassing things or by forking the entire codebase. If you +are interested in that, check out the :ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the section about :ref:`design`. -A Framework and an Example --------------------------- - -Flask is not only a microframework; it is also an example. Based on -Flask, there will be a series of blog posts that explain how to create a -framework. Flask itself is just one way to implement a framework on top -of existing libraries. Unlike many other microframeworks, Flask does not -try to implement everything on its own; it reuses existing code. - Web Development is Dangerous ---------------------------- From 718ef4d699d1de79117f92ff16feed98c3a1aa11 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 27 Aug 2011 00:32:28 +0200 Subject: [PATCH 0810/3747] Added an XXX to a comment to not miss removing deprecated code later --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 0943d10a..f4eb2188 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -91,7 +91,7 @@ class RequestContext(object): self.match_request() - # Support for deprecated functionality. This is doing away with + # XXX: Support for deprecated functionality. This is doing away with # Flask 1.0 blueprint = self.request.blueprint if blueprint is not None: From 08bf538fb4d173fffcda1397e8485b069c03e718 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 27 Aug 2011 00:36:53 +0200 Subject: [PATCH 0811/3747] Added a note on the behaviour of the routing system --- docs/design.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/design.rst b/docs/design.rst index 1f391b8c..6ca363a6 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -79,6 +79,22 @@ Furthermore this design makes it possible to use a factory function to create the application which is very helpful for unittesting and similar things (:ref:`app-factories`). +The Routing System +------------------ + +Flask uses the Werkzeug routing system which has was designed to +automatically order routes by complexity. This means that you can declare +routes in arbitrary order and they will still work as expected. This is a +requirement if you want to properly implement decorator based routing +since decorators could be fired in undefined order when the application is +split into multiple modules. + +Another design decision with the Werkzeug routing system is that routes +in Werkzeug try to ensure that there is that URLs are unique. Werkzeug +will go quite far with that in that it will automatically redirect to a +canonical URL if a route is ambiguous. + + One Template Engine ------------------- From bb1567dae48fed50f9742b9136c8bd1b82b7bd55 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 27 Aug 2011 00:42:06 +0200 Subject: [PATCH 0812/3747] Explained why os.getcwd is used for path finding --- flask/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index d8f7ac63..8f2dccf4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -484,6 +484,8 @@ def get_root_path(import_name): directory = os.path.dirname(sys.modules[import_name].__file__) return os.path.abspath(directory) except AttributeError: + # this is necessary in case we are running from the interactive + # python shell. It will never be used for production code however return os.getcwd() @@ -499,6 +501,7 @@ def find_package(import_name): root_mod = sys.modules[import_name.split('.')[0]] package_path = getattr(root_mod, '__file__', None) if package_path is None: + # support for the interactive python shell package_path = os.getcwd() else: package_path = os.path.abspath(os.path.dirname(package_path)) From 87f50fdc6f01160d75fcc87569f0754b800fc59f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 27 Aug 2011 00:44:46 +0200 Subject: [PATCH 0813/3747] Don't lie to the user about POST redirects --- flask/debughelpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index b4f73dd3..edf8c111 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -54,7 +54,8 @@ class FormDataRoutingRedirect(AssertionError): buf.append(' Make sure to directly send your %s-request to this URL ' 'since we can\'t make browsers or HTTP clients redirect ' - 'with form data.' % request.method) + 'with form data reliably or without user interaction.' % + request.method) buf.append('\n\nNote: this exception is only raised in debug mode') AssertionError.__init__(self, ''.join(buf).encode('utf-8')) From 23bf2633f67d00418bd31c0c6918c3a99f06dead Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 29 Aug 2011 12:18:25 +0200 Subject: [PATCH 0814/3747] Use the _request_ctx_stack instead of the proxy for consistency with the others. --- flask/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 1136469f..609ab952 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1397,7 +1397,7 @@ class Flask(_PackageBoundObject): This also triggers the :meth:`url_value_processor` functions before the actualy :meth:`before_request` functions are called. """ - bp = request.blueprint + bp = _request_ctx_stack.top.request.blueprint funcs = self.url_value_preprocessors.get(None, ()) if bp is not None and bp in self.url_value_preprocessors: @@ -1447,7 +1447,7 @@ class Flask(_PackageBoundObject): tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) - bp = request.blueprint + bp = _request_ctx_stack.top.request.blueprint if bp is not None and bp in self.teardown_request_funcs: funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) exc = sys.exc_info()[1] From 4dc1796b1c09961098edc0aaac90a28be2b7fc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bartoszkiewicz?= Date: Mon, 29 Aug 2011 21:37:52 +0200 Subject: [PATCH 0815/3747] Fixed session loading in flask.testing.TestClient.session_transaction() --- flask/testing.py | 2 +- flask/testsuite/testing.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/testing.py b/flask/testing.py index 612b4d4d..757e20cc 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -57,7 +57,7 @@ class FlaskClient(Client): raise RuntimeError('Session transactions only make sense ' 'with cookies enabled.') app = self.application - environ_overrides = kwargs.pop('environ_overrides', {}) + environ_overrides = kwargs.setdefault('environ_overrides', {}) self.cookie_jar.inject_wsgi(environ_overrides) outer_reqctx = _request_ctx_stack.top with app.test_request_context(*args, **kwargs) as c: diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 32867c3f..5ee147f9 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -59,6 +59,9 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(len(sess), 1) rv = c.get('/') self.assert_equal(rv.data, '[42]') + with c.session_transaction() as sess: + self.assert_equal(len(sess), 1) + self.assert_equal(sess['foo'], [42]) def test_session_transactions_no_null_sessions(self): app = flask.Flask(__name__) From ccf464189b116ea4ee458c2ccb24d64f9272e25b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 30 Aug 2011 14:36:50 +0200 Subject: [PATCH 0816/3747] Added finer control over the session cookie parameters --- CHANGES | 1 + docs/config.rst | 18 +++++++++++++++++- flask/app.py | 6 +++++- flask/sessions.py | 28 ++++++++++++++++++++++++---- flask/testsuite/basic.py | 22 ++++++++++++++++++++++ 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index b39a2001..2f47eab1 100644 --- a/CHANGES +++ b/CHANGES @@ -42,6 +42,7 @@ Relase date to be decided, codename to be chosen. pluggable (class based) views. - Fixed an issue where the test client if used with the with statement did not trigger the execution of the teardown handlers. +- Added finer control over the session cookie parameters. Version 0.7.3 ------------- diff --git a/docs/config.rst b/docs/config.rst index df31aba0..1ed004d2 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -70,6 +70,20 @@ The following configuration values are used internally by Flask: very risky). ``SECRET_KEY`` the secret key ``SESSION_COOKIE_NAME`` the name of the session cookie +``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If + this is not set, the cookie will be + valid for all subdomains of + ``SERVER_NAME``. +``SESSION_COOKIE_PATH`` the path for the session cookie. If + this is not set the cookie will be valid + for all of ``APPLICATION_ROOT`` or if + that is not set for ``'/'``. +``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set + with the httponly flag. Defaults to + `True`. +``SESSION_COOKIE_SECURE`` controls if the cookie should be set + with the secure flag. Defaults to + `False`. ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. ``USE_X_SENDFILE`` enable/disable x-sendfile @@ -142,7 +156,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.8 ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``, - ``APPLICATION_ROOT`` + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``, + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, + ``SESSION_COOKIE_SECURE`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 609ab952..3c479df5 100644 --- a/flask/app.py +++ b/flask/app.py @@ -231,12 +231,16 @@ class Flask(_PackageBoundObject): 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, - 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False diff --git a/flask/sessions.py b/flask/sessions.py index fda84a25..8a9fae51 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -123,16 +123,33 @@ class SessionInterface(object): """Helpful helper method that returns the cookie domain that should be used for the session cookie if session cookies are used. """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] if app.config['SERVER_NAME'] is not None: # chop of the port which is usually not supported by browsers return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] def get_cookie_path(self, app): """Returns the path for which the cookie should be valid. The - default implementation uses the value from the ``APPLICATION_ROOT`` - configuration variable or uses ``/`` if it's `None`. + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. """ - return app.config['APPLICATION_ROOT'] or '/' + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session @@ -177,9 +194,12 @@ class SecureCookieSessionInterface(SessionInterface): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) if session.modified and not session: response.delete_cookie(app.session_cookie_name, path=path, domain=domain) else: session.save_cookie(response, app.session_cookie_name, path=path, - expires=expires, httponly=True, domain=domain) + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 947d0073..33975b99 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -207,6 +207,28 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = app.test_client().get('/', 'http://example.com:8080/') self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + def test_session_using_session_settings(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_PATH='/' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + self.assert_('domain=.example.com' in cookie) + self.assert_('path=/;' in cookie) + self.assert_('secure' in cookie) + self.assert_('httponly' not in cookie) + def test_missing_session(self): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): From 5f7f3b17dfe2fbeecf82d5745c497b3ea94bf50a Mon Sep 17 00:00:00 2001 From: Christopher Currie Date: Tue, 30 Aug 2011 15:09:56 -0700 Subject: [PATCH 0817/3747] Fix for d5cd4f8d which broke install on Windows --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index db2eb9c7..5dfe3a87 100644 --- a/setup.py +++ b/setup.py @@ -89,9 +89,7 @@ setup( 'and good intentions', long_description=__doc__, packages=['flask', 'flask.testsuite'], - package_data={ - 'flask.testsuite': ['test_apps/*', 'static/*', 'templates/*'] - }, + include_package_data = True, zip_safe=False, platforms='any', install_requires=[ From ee8417dac8364f02524edffddff92329f15b95af Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 1 Sep 2011 16:57:00 +0200 Subject: [PATCH 0818/3747] Late but 2010 -> 2011 in some files --- flask/__init__.py | 2 +- flask/app.py | 2 +- flask/config.py | 2 +- flask/ctx.py | 2 +- flask/globals.py | 2 +- flask/helpers.py | 2 +- flask/logging.py | 2 +- flask/module.py | 2 +- flask/session.py | 2 +- flask/sessions.py | 2 +- flask/signals.py | 2 +- flask/templating.py | 2 +- flask/testing.py | 2 +- flask/wrappers.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 47bf3cab..c1076c33 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -6,7 +6,7 @@ A microframework based on Werkzeug. It's extensively documented and follows best practice patterns. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/app.py b/flask/app.py index 3c479df5..b3978e76 100644 --- a/flask/app.py +++ b/flask/app.py @@ -5,7 +5,7 @@ This module implements the central WSGI application object. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/config.py b/flask/config.py index 06dd02e2..7b6cd1ee 100644 --- a/flask/config.py +++ b/flask/config.py @@ -5,7 +5,7 @@ Implements the configuration related objects. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/ctx.py b/flask/ctx.py index f4eb2188..64293dc5 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -5,7 +5,7 @@ Implements the objects required to keep the context. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/globals.py b/flask/globals.py index 34099263..bcd08722 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -6,7 +6,7 @@ Defines all the global objects that are proxies to the current active context. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/helpers.py b/flask/helpers.py index 8f2dccf4..72c8f170 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -5,7 +5,7 @@ Implements various helpers. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/logging.py b/flask/logging.py index 8379ab66..b992aef8 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -5,7 +5,7 @@ Implements the logging support for Flask. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/module.py b/flask/module.py index 61b3cbc4..1c4f466c 100644 --- a/flask/module.py +++ b/flask/module.py @@ -5,7 +5,7 @@ Implements a class that represents module blueprints. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/session.py b/flask/session.py index bfe196b0..4d4d2cd6 100644 --- a/flask/session.py +++ b/flask/session.py @@ -6,7 +6,7 @@ This module used to flask with the session global so we moved it over to flask.sessions - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/sessions.py b/flask/sessions.py index 8a9fae51..2795bb1f 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -6,7 +6,7 @@ Implements cookie based sessions based on Werkzeug's secure cookie system. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/signals.py b/flask/signals.py index 984accb7..eeb763d4 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -6,7 +6,7 @@ Implements signals based on blinker if available, otherwise falls silently back to a noop - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ signals_available = False diff --git a/flask/templating.py b/flask/templating.py index d38d3824..90e8772a 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -5,7 +5,7 @@ Implements the bridge to Jinja2. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ import posixpath diff --git a/flask/testing.py b/flask/testing.py index 612b4d4d..474eba15 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -6,7 +6,7 @@ Implements test support helpers. This module is lazily imported and usually not used in production environments. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/flask/wrappers.py b/flask/wrappers.py index aed0a8d0..f6ec2788 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -5,7 +5,7 @@ Implements the WSGI wrappers (request and response). - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ From 37f9cb9ca69176dec36faff4b2138226b479f65f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 1 Sep 2011 12:35:04 -0400 Subject: [PATCH 0819/3747] Import with statement in testsuite, Python 2.5. --- flask/testing.py | 2 ++ flask/testsuite/__init__.py | 3 +++ flask/testsuite/basic.py | 3 +++ flask/testsuite/blueprints.py | 3 +++ flask/testsuite/deprecations.py | 3 +++ flask/testsuite/helpers.py | 3 +++ flask/testsuite/templating.py | 3 +++ flask/testsuite/testing.py | 3 +++ 8 files changed, 23 insertions(+) diff --git a/flask/testing.py b/flask/testing.py index 612b4d4d..67c3b434 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,6 +10,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 3f807251..49b85b23 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -9,6 +9,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import os import sys import flask diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 078027b9..e71ce75c 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import re import flask import unittest diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 73577961..3f65dd48 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import flask import unittest import warnings diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py index 062f40b0..795a5d3d 100644 --- a/flask/testsuite/deprecations.py +++ b/flask/testsuite/deprecations.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import flask import unittest from flask.testsuite import FlaskTestCase, catch_warnings diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 56264f70..052d36e1 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import os import flask import unittest diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index eadbdcf6..453bfb65 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import flask import unittest from flask.testsuite import FlaskTestCase diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 32867c3f..2b073fd3 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -8,6 +8,9 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ + +from __future__ import with_statement + import flask import unittest from flask.testsuite import FlaskTestCase From 7331ae3df58bdf322122010e598b7e7bb793daea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 3 Sep 2011 17:13:01 +0200 Subject: [PATCH 0820/3747] Update the testsuite to not freak out about a change in Werkzeug 0.8 that changed the behavior of misconfigured hosts. --- flask/testsuite/basic.py | 56 +++++++++++++--------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index e71ce75c..75286dbf 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -702,63 +702,43 @@ class BasicFunctionalityTestCase(FlaskTestCase): def subdomain(): return 'Foo SubDomain' - try: - rv = app.test_client().get('/') - self.assert_equal(rv.data, 'Foo') - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'Foo') - try: - rv = app.test_client().get('/', 'http://localhost.localdomain:5000') - self.assert_equal(rv.data, 'Foo') - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) + rv = app.test_client().get('/', 'http://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') - try: - rv = app.test_client().get('/', 'https://localhost.localdomain:5000') - self.assert_equal(rv.data, 'Foo') - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) + rv = app.test_client().get('/', 'https://localhost.localdomain:5000') + self.assert_equal(rv.data, 'Foo') - try: - app.config.update(SERVER_NAME='localhost.localdomain') - rv = app.test_client().get('/', 'https://localhost.localdomain') - self.assert_equal(rv.data, 'Foo') - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) + app.config.update(SERVER_NAME='localhost.localdomain') + rv = app.test_client().get('/', 'https://localhost.localdomain') + self.assert_equal(rv.data, 'Foo') try: app.config.update(SERVER_NAME='localhost.localdomain:443') rv = app.test_client().get('/', 'https://localhost.localdomain') - self.assert_equal(rv.data, 'Foo') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) except ValueError, e: + # Werkzeug 0.7 self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:443') does not match the " + \ "server name from the WSGI environment ('localhost.localdomain')") try: app.config.update(SERVER_NAME='localhost.localdomain') - app.test_client().get('/', 'http://foo.localhost') + rv = app.test_client().get('/', 'http://foo.localhost') + # Werkzeug 0.8 + self.assert_equal(rv.status_code, 404) except ValueError, e: + # Werkzeug 0.7 self.assert_equal(str(e), "the server name provided " + \ "('localhost.localdomain') does not match the " + \ "server name from the WSGI environment ('foo.localhost')") - try: - rv = app.test_client().get('/', 'http://foo.localhost.localdomain') - self.assert_equal(rv.data, 'Foo SubDomain') - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) + rv = app.test_client().get('/', 'http://foo.localhost.localdomain') + self.assert_equal(rv.data, 'Foo SubDomain') def test_exception_propagation(self): def apprunner(configkey): From b40af3ccd99ba5f392776073fbef9b6b3946e603 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Sep 2011 18:51:47 +0200 Subject: [PATCH 0821/3747] Updated the documentation to show how to set cookies for not yet existing responses --- docs/patterns/deferredcallbacks.rst | 73 +++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + docs/quickstart.rst | 4 ++ 3 files changed, 78 insertions(+) create mode 100644 docs/patterns/deferredcallbacks.rst diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst new file mode 100644 index 00000000..917c5125 --- /dev/null +++ b/docs/patterns/deferredcallbacks.rst @@ -0,0 +1,73 @@ +.. _deferred-callbacks: + +Deferred Request Callbacks +========================== + +One of the design principles of Flask is that response objects are created +and passed down a chain of potential callbacks that can modify them or +replace them. When the request handling starts, there is no response +object yet. It is created as necessary either by a view function or by +some other component in the system. + +But what happens if you want to modify the response at a point where the +response does not exist yet? A common example for that would be a +before-request function that wants to set a cookie on the response object. + +One way is to avoid the situation. Very often that is possible. For +instance you can try to move that logic into an after-request callback +instead. Sometimes however moving that code there is just not a very +pleasant experience or makes code look very awkward. + +As an alternative possibility you can attach a bunch of callback functions +to the :data:`~flask.g` object and call then at the end of the request. +This way you can defer code execution from anywhere in the application. + + +The Decorator +------------- + +The following decorator is the key. It registers a function on a list on +the :data:`~flask.g` object:: + + from flask import g + + def after_this_request(f): + if not hasattr(g, 'after_request_callbacks'): + g.after_request_callbacks = [] + g.after_request_callbacks.append(f) + return f + + +Calling the Deferred +-------------------- + +Now you can use the `after_this_request` decorator to mark a function to +be called at the end of the request. But we still need to call them. For +this the following function needs to be registered as +:meth:`~flask.Flask.after_request` callback:: + + @app.after_request + def call_after_request_callbacks(response): + for callback in getattr(g, 'after_request_callbacks', ()): + response = callback(response) + return response + + +A Practical Example +------------------- + +Now we can easily at any point in time register a function to be called at +the end of this particular request. For example you can remember the +current language of the user in a cookie in the before-request function:: + + from flask import request + + @app.before_request + def detect_user_language(): + language = request.cookies.get('user_lang') + if language is None: + language = guess_language_from_request() + @after_this_request + def remember_language(response): + response.set_cookie('user_lang', language) + g.language = language diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 3a1e409c..6f63df11 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -36,3 +36,4 @@ Snippet Archives `_. mongokit favicon streaming + cookies diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 690433db..34aa3be4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -621,6 +621,10 @@ just return strings from the view functions Flask will convert them into response objects for you. If you explicitly want to do that you can use the :meth:`~flask.make_response` function and then modify it. +Sometimes you might want to set a cookie at a point where the response +object does not exist yet. This is possible by utilizing the +:ref:`deferred-callbacks` pattern. + For this also see :ref:`about-responses`. Redirects and Errors From 88617311dba62992bf5736b35e3da9359ac5fedf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 5 Sep 2011 18:52:28 +0200 Subject: [PATCH 0822/3747] Added an testcase for subclassing of Flask to supress logging --- flask/testsuite/subclassing.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 flask/testsuite/subclassing.py diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py new file mode 100644 index 00000000..e56ad563 --- /dev/null +++ b/flask/testsuite/subclassing.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.subclassing + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test that certain behavior of flask can be customized by + subclasses. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import flask +import unittest +from StringIO import StringIO +from logging import StreamHandler +from flask.testsuite import FlaskTestCase + + +class FlaskSubclassingTestCase(FlaskTestCase): + + def test_supressed_exception_logging(self): + class SupressedFlask(flask.Flask): + def log_exception(self, exc_info): + pass + + out = StringIO() + app = SupressedFlask(__name__) + app.logger_name = 'flask_tests/test_supressed_exception_logging' + app.logger.addHandler(StreamHandler(out)) + + @app.route('/') + def index(): + 1/0 + + rv = app.test_client().get('/') + self.assert_equal(rv.status_code, 500) + self.assert_('Internal Server Error' in rv.data) + + err = out.getvalue() + self.assert_equal(err, '') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(FlaskSubclassingTestCase)) + return suite From 7d7d810aea5472101ce04956889dfdb77173a912 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 10 Sep 2011 23:20:58 +0200 Subject: [PATCH 0823/3747] Single quotes for consistency --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 9c0f0211..90dafcc0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -952,7 +952,7 @@ class Flask(_PackageBoundObject): :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - endpoint = options.pop("endpoint", None) + endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator From f6798885e6cc97bccd1e0fd861b54b01fcdb9605 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Sep 2011 21:22:23 -0400 Subject: [PATCH 0824/3747] Update create_global_jinja_loader docstring, #321. --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 90dafcc0..15f3ead2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -606,7 +606,7 @@ class Flask(_PackageBoundObject): """Creates the loader for the Jinja2 environment. Can be used to override just the loader and keeping the rest unchanged. It's discouraged to override this function. Instead one should override - the :meth:`create_jinja_loader` function instead. + the :meth:`jinja_loader` function instead. The global loader dispatches between the loaders of the application and the individual blueprints. From 17a46a4d239a640049081b5cdcc1ff20ef63e38a Mon Sep 17 00:00:00 2001 From: Joel Perras Date: Wed, 14 Sep 2011 16:53:05 -0300 Subject: [PATCH 0825/3747] Fixed typo in docstring of `dispatch_request` method of `flask.views.View`. --- flask/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index 2fe34622..60865cd4 100644 --- a/flask/views.py +++ b/flask/views.py @@ -64,7 +64,7 @@ class View(object): def dispatch_request(self): """Subclasses have to override this method to implement the - actual view functionc ode. This method is called with all + actual view function code. This method is called with all the arguments from the URL rule. """ raise NotImplementedError() From f8caa54d31605f8997698d5c6c295ff4cff9ecdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bartoszkiewicz?= Date: Sat, 17 Sep 2011 00:31:09 +0200 Subject: [PATCH 0826/3747] Fixed Blueprint.app_url_value_preprocessor. --- flask/blueprints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index 8bfc610e..ccdda38d 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -292,8 +292,8 @@ class Blueprint(_PackageBoundObject): def app_url_value_preprocessor(self, f): """Same as :meth:`url_value_preprocessor` but application wide. """ - self.record_once(lambda s: s.app.url_value_preprocessor - .setdefault(self.name, []).append(f)) + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(None, []).append(f)) return f def app_url_defaults(self, f): From 32c7e43dda6600828ae9b11111d722ab92fb0594 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Sep 2011 19:39:10 +0200 Subject: [PATCH 0827/3747] Simplified HEAD handling for method views --- CHANGES | 2 ++ flask/testsuite/views.py | 35 +++++++++++++++++++++++++++++++++++ flask/views.py | 6 +++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 2f47eab1..0e23b1fb 100644 --- a/CHANGES +++ b/CHANGES @@ -43,6 +43,8 @@ Relase date to be decided, codename to be chosen. - Fixed an issue where the test client if used with the with statement did not trigger the execution of the teardown handlers. - Added finer control over the session cookie parameters. +- HEAD requests to a method view now automatically dispatch to the `get` + method if no handler was implemented. Version 0.7.3 ------------- diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index a89c44ac..c7cb0a8a 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -110,6 +110,41 @@ class ViewTestCase(FlaskTestCase): self.assert_equal(rv.headers['X-Parachute'], 'awesome') self.assert_equal(rv.data, 'Awesome') + def test_implicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return flask.Response('Blub', headers={ + 'X-Method': flask.request.method + }) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'Blub') + self.assert_equal(rv.headers['X-Method'], 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + + def test_explicit_head(self): + app = flask.Flask(__name__) + + class Index(flask.views.MethodView): + def get(self): + return 'GET' + def head(self): + return flask.Response('', headers={'X-Method': 'HEAD'}) + + app.add_url_rule('/', view_func=Index.as_view('index')) + c = app.test_client() + rv = c.get('/') + self.assert_equal(rv.data, 'GET') + rv = c.head('/') + self.assert_equal(rv.data, '') + self.assert_equal(rv.headers['X-Method'], 'HEAD') + def suite(): suite = unittest.TestSuite() diff --git a/flask/views.py b/flask/views.py index 2fe34622..8f368bbc 100644 --- a/flask/views.py +++ b/flask/views.py @@ -143,5 +143,9 @@ class MethodView(View): def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) - assert meth is not None, 'Not implemented method' + # if the request method is HEAD and we don't have a handler for it + # retry with GET + if meth is None and request.method == 'HEAD': + meth = getattr(self, 'get', None) + assert meth is not None, 'Not implemented method %r' % request.method return meth(*args, **kwargs) From e68b34eb6c4636f7006a9d4c1172d5d09d6b2dc7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Sep 2011 19:39:19 +0200 Subject: [PATCH 0828/3747] Added API example to the method view docs --- docs/views.rst | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/views.rst b/docs/views.rst index 10ddb57d..441620a6 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -156,3 +156,72 @@ specify a list of decorators to apply in the class declaration:: Due to the implicit self from the caller's perspective you cannot use regular view decorators on the individual methods of the view however, keep this in mind. + +Method Views for APIs +--------------------- + +Web APIs are often working very closely with HTTP verbs so it makes a lot +of sense to implement such an API based on the +:class:`~flask.views.MethodView`. That said, you will notice that the API +will require different URL rules that go to the same method view most of +the time. For instance consider that you are exposing a user object on +the web: + +=============== =============== ====================================== +URL Method Description +--------------- --------------- -------------------------------------- +``/users/`` ``GET`` Gives a list of all users +``/users/`` ``POST`` Creates a new user +``/users/`` ``GET`` Shows a single user +``/users/`` ``PUT`` Updates a single user +``/users/`` ``DELETE`` Deletes a single user +=============== =============== ====================================== + +So how would you go about doing that with the +:class:`~flask.views.MethodView`? The trick is to take advantage of the +fact that you can provide multiple rules to the same view. + +Let's assume for the moment the view would look like this:: + + class UserAPI(MethodView): + + def get(self, user_id): + if user_id is None: + # return a list of users + pass + else: + # expose a single user + pass + + def post(self): + # create a new user + pass + + def delete(self, user_id): + # delete a single user + pass + + def put(self, user_id): + # update a single user + pass + +So how do we hook this up with the routing system? By adding two rules +and explicitly mentioning the methods for each:: + + user_view = UserAPI.as_view('user_api') + app.add_url_rule('/users/', defaults={'user_id': None}, + view_func=user_view, methods=['GET', 'POST']) + app.add_url_rule('/users/', view_func=user_view, + methods=['GET', 'PUT', 'DELETE']) + +If you have a lot of APIs that look similar you can refactor that +registration code:: + + def register_api(view, endpoint, url, pk='id', pk_type='int'): + view_func = view.as_view(endpoint) + app.add_url_rule(url, defaults={pk: None}, + view_func=view_func, methods=['GET', 'POST']) + app.add_url_rule('%s<%s:%s>' % (url, pk), view_func=view_func, + methods=['GET', 'PUT', 'DELETE']) + + register_api(UserAPI, 'user_api', '/users/', pk='user_id') From 8f85a3b0d161876de378b35ccac42237a18a5342 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 17 Sep 2011 22:11:57 +0200 Subject: [PATCH 0829/3747] Experimental redirect importing for flask.ext to flask_ and flaskext. --- flask/ext/__init__.py | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 flask/ext/__init__.py diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py new file mode 100644 index 00000000..2d7ba956 --- /dev/null +++ b/flask/ext/__init__.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" + flask.ext + ~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + imprt ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys + + +class _ExtensionImporter(object): + """This importer redirects imports from this submodule to other + locations. This makes it possible to transition from the old + flaskext.name to the newer flask_name without people having a + hard time. + """ + _module_choices = ['flask_%s', 'flaskext.%s'] + _modules = sys.modules + + def find_module(self, fullname, path=None): + if fullname.rsplit('.', 1)[0] == __name__: + return self + + def load_module(self, fullname): + if fullname in self._modules: + return self._modules[fullname] + packname, modname = fullname.rsplit('.', 1) + for path in self._module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + continue + module = self._modules[fullname] = self._modules[realname] + setattr(self._modules[__name__], modname, module) + module.__loader__ = self + return module + raise ImportError(fullname) + + +sys.meta_path.append(_ExtensionImporter()) +del sys, _ExtensionImporter From c72ca16234e4dbea448ea43d002eb500c09f4932 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 02:10:10 +0200 Subject: [PATCH 0830/3747] Added tests for the import hook and fixed a problem with it. --- flask/ext/__init__.py | 40 +++++++----- flask/testsuite/ext.py | 61 +++++++++++++++++++ .../flask_newext_package/__init__.py | 1 + .../flask_newext_package/submodule.py | 2 + .../test_apps/flask_newext_simple.py | 1 + 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 flask/testsuite/ext.py create mode 100644 flask/testsuite/test_apps/flask_newext_package/__init__.py create mode 100644 flask/testsuite/test_apps/flask_newext_package/submodule.py create mode 100644 flask/testsuite/test_apps/flask_newext_simple.py diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 2d7ba956..742e35dd 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -8,7 +8,7 @@ force all extensions to upgrade at the same time. When a user does ``from flask.ext.foo import bar`` it will attempt to - imprt ``from flask_foo import bar`` first and when that fails it will + import ``from flask_foo import bar`` first and when that fails it will try to import ``from flaskext.foo import bar``. We're switching from namespace packages because it was just too painful for @@ -17,38 +17,46 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -import sys class _ExtensionImporter(object): - """This importer redirects imports from this submodule to other - locations. This makes it possible to transition from the old - flaskext.name to the newer flask_name without people having a - hard time. + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. """ _module_choices = ['flask_%s', 'flaskext.%s'] - _modules = sys.modules + + def __init__(self): + from sys import meta_path + self.prefix = __name__ + '.' + self.prefix_cutoff = __name__.count('.') + 1 + + def _name(x): + cls = type(x) + return cls.__module__ + '.' + cls.__name__ + this = _name(self) + meta_path[:] = [x for x in meta_path if _name(x) != this] + [self] def find_module(self, fullname, path=None): - if fullname.rsplit('.', 1)[0] == __name__: + if fullname.startswith(self.prefix): return self def load_module(self, fullname): - if fullname in self._modules: - return self._modules[fullname] - packname, modname = fullname.rsplit('.', 1) + from sys import modules + if fullname in modules: + return modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] for path in self._module_choices: realname = path % modname try: __import__(realname) except ImportError: continue - module = self._modules[fullname] = self._modules[realname] - setattr(self._modules[__name__], modname, module) - module.__loader__ = self + module = modules[fullname] = modules[realname] + setattr(modules[__name__], modname, module) return module raise ImportError(fullname) -sys.meta_path.append(_ExtensionImporter()) -del sys, _ExtensionImporter +_ExtensionImporter() +del _ExtensionImporter diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py new file mode 100644 index 00000000..ad8d0d16 --- /dev/null +++ b/flask/testsuite/ext.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.ext + ~~~~~~~~~~~~~~~~~~~ + + Tests the extension import thing. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import unittest +from flask.testsuite import FlaskTestCase + + +class ExtImportHookTestCase(FlaskTestCase): + + def setup(self): + for entry, value in sys.modules.items(): + if entry.startswith('flask.ext.') and value is not None: + sys.modules.pop(entry, None) + from flask import ext + reload(ext) + + # reloading must not add more hooks + import_hooks = 0 + for item in sys.meta_path: + cls = type(item) + if cls.__module__ == 'flask.ext' and \ + cls.__name__ == '_ExtensionImporter': + import_hooks += 1 + self.assert_equal(import_hooks, 1) + + def test_flaskext_simple_import_normal(self): + from flask.ext.newext_simple import ext_id + self.assert_equal(ext_id, 'newext_simple') + + def test_flaskext_simple_import_module(self): + from flask.ext import newext_simple + self.assert_equal(newext_simple.ext_id, 'newext_simple') + self.assert_equal(newext_simple.__name__, 'flask_newext_simple') + + def test_flaskext_package_import_normal(self): + from flask.ext.newext_package import ext_id + self.assert_equal(ext_id, 'newext_package') + + def test_flaskext_package_import_module(self): + from flask.ext import newext_package + self.assert_equal(newext_package.ext_id, 'newext_package') + self.assert_equal(newext_package.__name__, 'flask_newext_package') + + def test_flaskext_package_import_submodule(self): + from flask.ext.newext_package import submodule + self.assert_equal(submodule.__name__, 'flask_newext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(ExtImportHookTestCase)) + return suite diff --git a/flask/testsuite/test_apps/flask_newext_package/__init__.py b/flask/testsuite/test_apps/flask_newext_package/__init__.py new file mode 100644 index 00000000..3fd13e17 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'newext_package' diff --git a/flask/testsuite/test_apps/flask_newext_package/submodule.py b/flask/testsuite/test_apps/flask_newext_package/submodule.py new file mode 100644 index 00000000..26ad56b7 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/flask/testsuite/test_apps/flask_newext_simple.py b/flask/testsuite/test_apps/flask_newext_simple.py new file mode 100644 index 00000000..dc4a3628 --- /dev/null +++ b/flask/testsuite/test_apps/flask_newext_simple.py @@ -0,0 +1 @@ +ext_id = 'newext_simple' From f80bfcaa28da98972002fb906c2559babe75801e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 03:26:46 +0200 Subject: [PATCH 0831/3747] Added tests for old imports --- flask/testsuite/ext.py | 33 ++++++++++++++++--- .../testsuite/test_apps/flaskext/__init__.py | 0 .../flaskext/oldext_package/__init__.py | 1 + .../flaskext/oldext_package/submodule.py | 2 ++ .../test_apps/flaskext/oldext_simple.py | 1 + 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 flask/testsuite/test_apps/flaskext/__init__.py create mode 100644 flask/testsuite/test_apps/flaskext/oldext_package/__init__.py create mode 100644 flask/testsuite/test_apps/flaskext/oldext_package/submodule.py create mode 100644 flask/testsuite/test_apps/flaskext/oldext_simple.py diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index ad8d0d16..41bbabeb 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -31,29 +31,52 @@ class ExtImportHookTestCase(FlaskTestCase): import_hooks += 1 self.assert_equal(import_hooks, 1) - def test_flaskext_simple_import_normal(self): + def test_flaskext_new_simple_import_normal(self): from flask.ext.newext_simple import ext_id self.assert_equal(ext_id, 'newext_simple') - def test_flaskext_simple_import_module(self): + def test_flaskext_new_simple_import_module(self): from flask.ext import newext_simple self.assert_equal(newext_simple.ext_id, 'newext_simple') self.assert_equal(newext_simple.__name__, 'flask_newext_simple') - def test_flaskext_package_import_normal(self): + def test_flaskext_new_package_import_normal(self): from flask.ext.newext_package import ext_id self.assert_equal(ext_id, 'newext_package') - def test_flaskext_package_import_module(self): + def test_flaskext_new_package_import_module(self): from flask.ext import newext_package self.assert_equal(newext_package.ext_id, 'newext_package') self.assert_equal(newext_package.__name__, 'flask_newext_package') - def test_flaskext_package_import_submodule(self): + def test_flaskext_new_package_import_submodule(self): from flask.ext.newext_package import submodule self.assert_equal(submodule.__name__, 'flask_newext_package.submodule') self.assert_equal(submodule.test_function(), 42) + def test_flaskext_old_simple_import_normal(self): + from flask.ext.oldext_simple import ext_id + self.assert_equal(ext_id, 'oldext_simple') + + def test_flaskext_old_simple_import_module(self): + from flask.ext import oldext_simple + self.assert_equal(oldext_simple.ext_id, 'oldext_simple') + self.assert_equal(oldext_simple.__name__, 'flaskext.oldext_simple') + + def test_flaskext_old_package_import_normal(self): + from flask.ext.oldext_package import ext_id + self.assert_equal(ext_id, 'oldext_package') + + def test_flaskext_old_package_import_module(self): + from flask.ext import oldext_package + self.assert_equal(oldext_package.ext_id, 'oldext_package') + self.assert_equal(oldext_package.__name__, 'flaskext.oldext_package') + + def test_flaskext_old_package_import_submodule(self): + from flask.ext.oldext_package import submodule + self.assert_equal(submodule.__name__, 'flaskext.oldext_package.submodule') + self.assert_equal(submodule.test_function(), 42) + def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/test_apps/flaskext/__init__.py b/flask/testsuite/test_apps/flaskext/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py b/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py new file mode 100644 index 00000000..7c462065 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_package/__init__.py @@ -0,0 +1 @@ +ext_id = 'oldext_package' diff --git a/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py b/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py new file mode 100644 index 00000000..26ad56b7 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_package/submodule.py @@ -0,0 +1,2 @@ +def test_function(): + return 42 diff --git a/flask/testsuite/test_apps/flaskext/oldext_simple.py b/flask/testsuite/test_apps/flaskext/oldext_simple.py new file mode 100644 index 00000000..c6664a78 --- /dev/null +++ b/flask/testsuite/test_apps/flaskext/oldext_simple.py @@ -0,0 +1 @@ +ext_id = 'oldext_simple' From e8020e2c5cd6dca816991845bd439e4524594f83 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 12:22:15 +0200 Subject: [PATCH 0832/3747] Added comment to the importer wiping. --- flask/ext/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 742e35dd..078e7dbd 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -31,6 +31,11 @@ class _ExtensionImporter(object): self.prefix = __name__ + '.' self.prefix_cutoff = __name__.count('.') + 1 + # since people might reload the flask.ext module (by accident or + # intentionally) we have to make sure to not add more than one + # import hook. We can't check class types here either since a new + # class will be created on reload. As a result of that we check + # the name of the class and remove stale instances. def _name(x): cls = type(x) return cls.__module__ + '.' + cls.__name__ From 9df2aefd7f0c3c5d0fc47087da2f51b72cad38fc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 12:22:42 +0200 Subject: [PATCH 0833/3747] Do not set dotted attributes on flask.ext --- flask/ext/__init__.py | 3 ++- flask/testsuite/ext.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 078e7dbd..14c8d8d4 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -58,7 +58,8 @@ class _ExtensionImporter(object): except ImportError: continue module = modules[fullname] = modules[realname] - setattr(modules[__name__], modname, module) + if '.' not in modname: + setattr(modules[__name__], modname, module) return module raise ImportError(fullname) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 41bbabeb..a64b9bd2 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -31,6 +31,11 @@ class ExtImportHookTestCase(FlaskTestCase): import_hooks += 1 self.assert_equal(import_hooks, 1) + def teardown(self): + from flask import ext + for key in ext.__dict__: + self.assert_('.' not in key) + def test_flaskext_new_simple_import_normal(self): from flask.ext.newext_simple import ext_id self.assert_equal(ext_id, 'newext_simple') From 8840973a337757ba5b7dab0df00a82d8489d3a06 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 12:50:56 +0200 Subject: [PATCH 0834/3747] Fixed a cross reference in the docs. --- docs/patterns/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 6f63df11..964b1e17 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -36,4 +36,4 @@ Snippet Archives `_. mongokit favicon streaming - cookies + deferredcallbacks From 1e6c5f0975d2ddef4430294c555b1deebb18f93b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 12:51:10 +0200 Subject: [PATCH 0835/3747] Documented flask.ext. --- CHANGES | 1 + docs/api.rst | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGES b/CHANGES index 0e23b1fb..79a669f0 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,7 @@ Relase date to be decided, codename to be chosen. - Added finer control over the session cookie parameters. - HEAD requests to a method view now automatically dispatch to the `get` method if no handler was implemented. +- Implemented the virtual :mod:`flask.ext` package to import extensions from. Version 0.7.3 ------------- diff --git a/docs/api.rst b/docs/api.rst index 1dc25eb1..aca7fda5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -341,6 +341,23 @@ Configuration .. autoclass:: Config :members: +Extensions +---------- + +.. data:: flask.ext + + This module acts as redirect import module to Flask extensions. It was + added in 0.8 as the canonical way to import Flask extensions and makes + it possible for us to have more flexibility in how we distribute + extensions. + + If you want to use an extension named “Flask-Foo” you would import it + from :data:`~flask.ext` as follows:: + + from flask.ext import foo + + .. versionadded:: 0.8 + Useful Internals ---------------- From 0719ad5f4fa7d52d38706b3ee60ef91dcfbaa98d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 13:03:05 +0200 Subject: [PATCH 0836/3747] Documented new import system for extensions --- docs/extensiondev.rst | 80 +++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 6b407b34..ee0d5e60 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -16,10 +16,10 @@ extension to behave. Anatomy of an Extension ----------------------- -Extensions are all located in a package called ``flaskext.something`` +Extensions are all located in a package called ``flask_something`` where "something" is the name of the library you want to bridge. So for example if you plan to add support for a library named `simplexml` to -Flask, you would name your extension's package ``flaskext.simplexml``. +Flask, you would name your extension's package ``flask_simplexml``. The name of the actual extension (the human readable name) however would be something like "Flask-SimpleXML". Make sure to include the name @@ -27,9 +27,11 @@ be something like "Flask-SimpleXML". Make sure to include the name This is how users can then register dependencies to your extension in their `setup.py` files. -The magic that makes it possible to have your library in a package called -``flaskext.something`` is called a "namespace package". Check out the -guide below how to create something like that. +Flask sets up a redirect package called :data:`flask.ext` where users +should import the extensions from. If you for instance have a package +called ``flask_something`` users would import it as +``flask.ext.something``. This is done to transition from the old +namespace packages. See :ref:`ext-import-transition` for more details. But how do extensions look like themselves? An extension has to ensure that it works with multiple Flask application instances at once. This is @@ -54,35 +56,15 @@ reviewed upfront if they behave as required. So let's get started with creating such a Flask extension. The extension we want to create here will provide very basic support for SQLite3. -There is a script on github called `Flask Extension Wizard`_ which helps -you create the initial folder structure. But for this very basic example -we want to create all by hand to get a better feeling for it. - First we create the following folder structure:: flask-sqlite3/ - flaskext/ - __init__.py - sqlite3.py - setup.py + flask_sqlite3.py LICENSE + README Here's the contents of the most important files: -flaskext/__init__.py -```````````````````` - -The only purpose of this file is to mark the package as namespace package. -This is required so that multiple modules from different PyPI packages can -reside in the same Python package:: - - __import__('pkg_resources').declare_namespace(__name__) - -If you want to know exactly what is happening there, checkout the -distribute or setuptools docs which explain how this works. - -Just make sure to not put anything else in there! - setup.py ```````` @@ -108,9 +90,12 @@ something you can work with:: author_email='your-email@example.com', description='Very short description', long_description=__doc__, - packages=['flaskext'], - namespace_packages=['flaskext'], + py_modules=['flask_sqlite3'], + # if you would be using a package instead use packages instead + # of py_modules: + # packages=['flask_sqlite3'], zip_safe=False, + include_package_data=True, platforms='any', install_requires=[ 'Flask' @@ -127,11 +112,10 @@ something you can work with:: ) That's a lot of code but you can really just copy/paste that from existing -extensions and adapt. This is also what the wizard creates for you if you -use it. +extensions and adapt. -flaskext/sqlite3.py -``````````````````` +flask_sqlite3.py +```````````````` Now this is where your extension code goes. But how exactly should such an extension look like? What are the best practices? Continue reading @@ -170,7 +154,7 @@ manager object that handles opening and closing database connections. The Extension Code ------------------ -Here's the contents of the `flaskext/sqlite3.py` for copy/paste:: +Here's the contents of the `flask_sqlite3.py` for copy/paste:: from __future__ import absolute_import import sqlite3 @@ -223,7 +207,7 @@ So why did we decide on a class based approach here? Because using our extension looks something like this:: from flask import Flask - from flaskext.sqlite3 import SQLite3 + from flask_sqlite3 import SQLite3 app = Flask(__name__) app.config.from_pyfile('the-config.cfg') @@ -343,7 +327,8 @@ Extension Registry`_ and marked appropriately. If you want your own extension to be approved you have to follow these guidelines: 1. An approved Flask extension must provide exactly one package or module - inside the `flaskext` namespace package. + named ``flask_extensionname``. They might also reside inside a + ``flaskext`` namespace packages though this is discouraged now. 2. It must ship a testing suite that can either be invoked with ``make test`` or ``python setup.py test``. For test suites invoked with ``make test`` the extension has to ensure that all dependencies for the test @@ -376,8 +361,27 @@ extension to be approved you have to follow these guidelines: Python 2.7 -.. _Flask Extension Wizard: - http://github.com/mitsuhiko/flask-extension-wizard +.. _ext-import-transition: + +Extension Import Transition +--------------------------- + +For a while we recommended using namespace packages for Flask extensions. +This turned out to be problematic in practice because many different +competing namespace package systems exist and pip would automatically +switch between different systems and this caused a lot of problems for +users. + +Instead we now recommend naming packages ``flask_foo`` instead of the now +deprecated ``flaskext.foo``. Flask 0.8 introduces a redirect import +system that lets uses import from ``flask.ext.foo`` and it will try +``flask_foo`` first and if that fails ``flaskext.foo``. + +Flask extensions should urge users to import from ``flask.ext.foo`` +instead of ``flask_foo`` or ``flaskext_foo`` so that extensions can +transition to the new package name without affecting users. + + .. _OAuth extension: http://packages.python.org/Flask-OAuth/ .. _mailinglist: http://flask.pocoo.org/mailinglist/ .. _IRC channel: http://flask.pocoo.org/community/irc/ From e94416704d70d64b21afb44569039f2b50db966f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 15:03:35 +0200 Subject: [PATCH 0837/3747] Added the flaskext_compat module to support flask.ext in 0.7 and earlier. --- scripts/flaskext_compat.py | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 scripts/flaskext_compat.py diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py new file mode 100644 index 00000000..ddc7e097 --- /dev/null +++ b/scripts/flaskext_compat.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" + flaskext_compat + ~~~~~~~~~~~~~~~ + + Implements the ``flask.ext`` virtual package for versions of Flask + older than 0.7. This module is a noop if Flask 0.8 was detected. + + Usage:: + + import flaskext_compat + from flask.ext import foo + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +class _ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + _module_choices = ['flask_%s', 'flaskext.%s'] + + def __init__(self): + from sys import meta_path + self.prefix = __name__ + '.' + self.prefix_cutoff = __name__.count('.') + 1 + + # since people might reload the flask.ext module (by accident or + # intentionally) we have to make sure to not add more than one + # import hook. We can't check class types here either since a new + # class will be created on reload. As a result of that we check + # the name of the class and remove stale instances. + def _name(x): + cls = type(x) + return cls.__module__ + '.' + cls.__name__ + this = _name(self) + meta_path[:] = [x for x in meta_path if _name(x) != this] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + from sys import modules + if fullname in modules: + return modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self._module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + continue + module = modules[fullname] = modules[realname] + if '.' not in modname: + setattr(modules[__name__], modname, module) + return module + raise ImportError(fullname) + + +import sys +import flask +try: + __import__('flask.ext') +except ImportError: + sys.modules['flask.ext'] = flask.ext = sys.modules[__name__] + __name__ = __package__ = 'flask.ext' + __path__ = [] + _ExtensionImporter() +del _ExtensionImporter, sys, flask From 933d203828164ef4eabcce29667207deb2409569 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 15:10:50 +0200 Subject: [PATCH 0838/3747] Started extensions introduction document --- docs/contents.rst.inc | 1 + docs/extensions.rst | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 docs/extensions.rst diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 7a8ebb12..a8ebc0d7 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -20,6 +20,7 @@ instructions for web development with Flask. views reqcontext blueprints + extensions shell patterns/index deploying/index diff --git a/docs/extensions.rst b/docs/extensions.rst new file mode 100644 index 00000000..9d00468a --- /dev/null +++ b/docs/extensions.rst @@ -0,0 +1,48 @@ +Flask Extensions +================ + +Flask extensions extend the functionality of Flask in various different +ways. For instance they add support for databases and other common tasks. + +Finding Extensions +------------------ + +Flask extensions are listed on the `Flask Extension Registry`_ and can be +downloaded with ``easy_install`` or ``pip``. If you add a Flask extension +as dependency to your ``requirements.rst`` or ``setup.py`` file they are +usually installed with a simple command or when your application installs. + +Using Extensions +---------------- + +Extensions typically have documentation that goes along that shows how to +use it. There are no general rules in how extensions are supposed to +behave but they are imported from common locations. If you have an +extension called ``Flask-Foo`` or ``Foo-Flask`` it will be always +importable from ``flask.ext.foo``:: + + from flask.ext import foo + +Flask < 0.8 +----------- + +If you are using Flask 0.7 or earlier the :data:`flask.ext` package will not +exist, instead you have to import from ``flaskext.foo`` or ``flask_foo`` +depending on how the extension is distributed. + +We recommend importing from ``flask.ext`` even with older versions of +Flask however. If you have an application that needs to work with +versions of Flask older than 0.8 you should activate the +``flaskext_compat`` module which provides the ``flask.ext`` module. You +can download it from github: `flaskext_compat.py`_ + +You can use it like this:: + + import flaskext_compat + from flask.ext import foo + +Once the ``flaskext_compat`` module is imported the :data:`flask.ext` will +exist and you can start importing from there. + +.. _Flask Extension Registry: http://flask.pocoo.org/extensions/ +.. _flaskext_compat.py: https://github.com/mitsuhiko/flask/raw/master/scripts/flaskext_compat.py From 9c82840f5256e8aca82c26ab4025712e5c928977 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Sep 2011 18:16:50 +0200 Subject: [PATCH 0839/3747] Added more tests for the import hook --- flask/testsuite/ext.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index a64b9bd2..dc90952f 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -17,7 +17,9 @@ class ExtImportHookTestCase(FlaskTestCase): def setup(self): for entry, value in sys.modules.items(): - if entry.startswith('flask.ext.') and value is not None: + if (entry.startswith('flask.ext.') or + entry.startswith('flask_') or + entry.startswith('flaskext.')) and value is not None: sys.modules.pop(entry, None) from flask import ext reload(ext) @@ -54,6 +56,10 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_equal(newext_package.ext_id, 'newext_package') self.assert_equal(newext_package.__name__, 'flask_newext_package') + def test_flaskext_new_package_import_submodule_function(self): + from flask.ext.newext_package.submodule import test_function + self.assert_equal(test_function(), 42) + def test_flaskext_new_package_import_submodule(self): from flask.ext.newext_package import submodule self.assert_equal(submodule.__name__, 'flask_newext_package.submodule') @@ -82,6 +88,10 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_equal(submodule.__name__, 'flaskext.oldext_package.submodule') self.assert_equal(submodule.test_function(), 42) + def test_flaskext_old_package_import_submodule_function(self): + from flask.ext.oldext_package.submodule import test_function + self.assert_equal(test_function(), 42) + def suite(): suite = unittest.TestSuite() From d4d6cc8401b94ab7c92b70ca5e5b21d22636dd5d Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Sun, 18 Sep 2011 13:05:15 +0300 Subject: [PATCH 0840/3747] Clean up auditing command --- setup.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 5dfe3a87..255f1e77 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ class run_audit(Command): user_options = [] def initialize_options(self): - all = None + pass def finalize_options(self): pass @@ -62,22 +62,19 @@ class run_audit(Command): print "Audit requires PyFlakes installed in your system.""" sys.exit(-1) - dirs = ['flask', 'tests'] - # Add example directories - for dir in ['flaskr', 'jqueryexample', 'minitwit']: - dirs.append(os.path.join('examples', dir)) - # TODO: Add test subdirectories warns = 0 + # Define top-level directories + dirs = ('flask', 'examples', 'scripts') for dir in dirs: - for filename in os.listdir(dir): - if filename.endswith('.py') and filename != '__init__.py': - warns += flakes.checkPath(os.path.join(dir, filename)) + for root, _, files in os.walk(dir): + for file in files: + if file != '__init__.py' and file.endswith('.py') : + warns += flakes.checkPath(os.path.join(root, file)) if warns > 0: print ("Audit finished with total %d warnings." % warns) else: print ("No problems found in sourcecode.") - setup( name='Flask', version='0.8-dev', From 907c24e6ffc436971d437b0b9122b840db9466d7 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Sat, 23 Jul 2011 11:32:59 -0700 Subject: [PATCH 0841/3747] Document the debug param for Flask.run, it is not part of **options given to run_simple. I am not sure bool() is appropriate. --- flask/app.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index 15f3ead2..4c8175d1 100644 --- a/flask/app.py +++ b/flask/app.py @@ -657,7 +657,7 @@ class Flask(_PackageBoundObject): # existing views. context.update(orig_ctx) - def run(self, host='127.0.0.1', port=5000, **options): + def run(self, host='127.0.0.1', port=5000, debug=None, **options): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. @@ -680,14 +680,15 @@ class Flask(_PackageBoundObject): :param host: the hostname to listen on. set this to ``'0.0.0.0'`` to have the server available externally as well. :param port: the port of the webserver + :param debug: if given, enable or disable debug mode. :param options: the options to be forwarded to the underlying Werkzeug server. See :func:`werkzeug.serving.run_simple` for more information. """ from werkzeug.serving import run_simple - if 'debug' in options: - self.debug = options.pop('debug') + if debug is not None: + self.debug = bool(debug) options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) try: From 90884a78fbba4068fa93f87626baccd148cd4a57 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 18 Sep 2011 15:34:37 -0400 Subject: [PATCH 0842/3747] Cross-reference debug docs in run docstring. --- flask/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/app.py b/flask/app.py index 4c8175d1..1ad669e3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -681,6 +681,7 @@ class Flask(_PackageBoundObject): to have the server available externally as well. :param port: the port of the webserver :param debug: if given, enable or disable debug mode. + See :attr:`debug`. :param options: the options to be forwarded to the underlying Werkzeug server. See :func:`werkzeug.serving.run_simple` for more From 7997bae5bb048e4623647c57faae447b28da907f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Sep 2011 21:00:28 +0200 Subject: [PATCH 0843/3747] Explicitly activate compat module --- docs/extensions.rst | 8 ++++-- scripts/flaskext_compat.py | 56 +++++++++++++++----------------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/docs/extensions.rst b/docs/extensions.rst index 9d00468a..5ae80457 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -33,12 +33,14 @@ depending on how the extension is distributed. We recommend importing from ``flask.ext`` even with older versions of Flask however. If you have an application that needs to work with versions of Flask older than 0.8 you should activate the -``flaskext_compat`` module which provides the ``flask.ext`` module. You -can download it from github: `flaskext_compat.py`_ +``flaskext_compat`` module which provides the ``flask.ext`` module if +you activate it. You can download it from github: `flaskext_compat.py`_ -You can use it like this:: +And here is how you can use it:: import flaskext_compat + flaskext_compat.activate() + from flask.ext import foo Once the ``flaskext_compat`` module is imported the :data:`flask.ext` will diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index ddc7e097..03601a1a 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -14,39 +14,30 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import sys +import imp + + +ext_module = imp.new_module('flask.ext') +ext_module.__path__ = [] +ext_module.__package__ = ext_module.__name__ class _ExtensionImporter(object): - """This importer redirects imports from this submodule to other locations. - This makes it possible to transition from the old flaskext.name to the - newer flask_name without people having a hard time. + """This importer redirects imports from the flask.ext module to other + locations. """ _module_choices = ['flask_%s', 'flaskext.%s'] - - def __init__(self): - from sys import meta_path - self.prefix = __name__ + '.' - self.prefix_cutoff = __name__.count('.') + 1 - - # since people might reload the flask.ext module (by accident or - # intentionally) we have to make sure to not add more than one - # import hook. We can't check class types here either since a new - # class will be created on reload. As a result of that we check - # the name of the class and remove stale instances. - def _name(x): - cls = type(x) - return cls.__module__ + '.' + cls.__name__ - this = _name(self) - meta_path[:] = [x for x in meta_path if _name(x) != this] + [self] + prefix = ext_module.__name__ + '.' + prefix_cutoff = prefix.count('.') def find_module(self, fullname, path=None): if fullname.startswith(self.prefix): return self def load_module(self, fullname): - from sys import modules - if fullname in modules: - return modules[fullname] + if fullname in sys.modules: + return sys.modules[fullname] modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] for path in self._module_choices: realname = path % modname @@ -54,20 +45,17 @@ class _ExtensionImporter(object): __import__(realname) except ImportError: continue - module = modules[fullname] = modules[realname] + module = sys.modules[fullname] = sys.modules[realname] if '.' not in modname: - setattr(modules[__name__], modname, module) + setattr(ext_module, modname, module) return module raise ImportError(fullname) -import sys -import flask -try: - __import__('flask.ext') -except ImportError: - sys.modules['flask.ext'] = flask.ext = sys.modules[__name__] - __name__ = __package__ = 'flask.ext' - __path__ = [] - _ExtensionImporter() -del _ExtensionImporter, sys, flask +def activate(): + """Activates the compatibility system.""" + import flask + if hasattr(flask, 'ext'): + return + sys.modules['flask.ext'] = flask.ext = ext_module + sys.meta_path.append(_ExtensionImporter()) From 2b8ea40cd63b7db86d73085d27b9a113688d7053 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 19 Sep 2011 21:00:44 +0200 Subject: [PATCH 0844/3747] import -> activate --- docs/extensions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensions.rst b/docs/extensions.rst index 5ae80457..f9870ea1 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -43,7 +43,7 @@ And here is how you can use it:: from flask.ext import foo -Once the ``flaskext_compat`` module is imported the :data:`flask.ext` will +Once the ``flaskext_compat`` module is activated the :data:`flask.ext` will exist and you can start importing from there. .. _Flask Extension Registry: http://flask.pocoo.org/extensions/ From 54875e93173f96d44a73431a4929059c55aed797 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Sep 2011 00:03:24 +0200 Subject: [PATCH 0845/3747] Direct passthrough is counter productive --- docs/patterns/streaming.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index 1998799f..8393b00b 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -13,10 +13,7 @@ Basic Usage This is a basic view function that generates a lot of CSV data on the fly. The trick is to have an inner function that uses a generator to generate -data and to then invoke that function and pass it to a response object -that has the ``direct_passthrough`` flag set. This flag is used to inform -the system that data is generated on the fly and should be passed through -without buffering:: +data and to then invoke that function and pass it to a response object:: from flask import Response @@ -25,8 +22,7 @@ without buffering:: def generate(): for row in iter_all_rows(): yield ','.join(row) + '\n' - return Response(generate(), direct_passthrough=True, - mimetype='text/csv') + return Response(generate(), mimetype='text/csv') Each ``yield`` expression is directly sent to the browser. Now though that some WSGI middlewares might break streaming, so be careful there in @@ -51,8 +47,7 @@ quite uncommon, but you can easily do it yourself:: @app.route('/my-large-page.html') def render_large_template(): rows = iter_all_rows() - return Response(stream_template('the_template.html', rows=rows), - direct_passthrough=True) + return Response(stream_template('the_template.html', rows=rows)) The trick here is to get the template object from the Jinja2 environment on the application and to call :meth:`~jinja2.Template.stream` instead of From 457371519545711214a82e2dc6ca8907e3263151 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 20 Sep 2011 18:57:37 +0200 Subject: [PATCH 0846/3747] Reworded before 0.8 ext docs. --- docs/extensions.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/extensions.rst b/docs/extensions.rst index f9870ea1..53dca56e 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -23,18 +23,16 @@ importable from ``flask.ext.foo``:: from flask.ext import foo -Flask < 0.8 ------------ +Flask Before 0.8 +---------------- If you are using Flask 0.7 or earlier the :data:`flask.ext` package will not exist, instead you have to import from ``flaskext.foo`` or ``flask_foo`` -depending on how the extension is distributed. - -We recommend importing from ``flask.ext`` even with older versions of -Flask however. If you have an application that needs to work with -versions of Flask older than 0.8 you should activate the -``flaskext_compat`` module which provides the ``flask.ext`` module if -you activate it. You can download it from github: `flaskext_compat.py`_ +depending on how the extension is distributed. If you want to develop an +application that supports Flask 0.7 or earlier you should still import +from the :data:`flask.ext` package. We provide you with a compatibility +module that provides this package for older versions of Flask. You can +download it from github: `flaskext_compat.py`_ And here is how you can use it:: From 9a70e62b8d545c8de7e3728e544cebe289a39254 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Sep 2011 03:47:36 +0200 Subject: [PATCH 0847/3747] No longer swallow important import errors. This implements a simple but elegant method to find out if an import error can be swallowed to try the next module or if the import error is important and must be reraised. --- flask/ext/__init__.py | 20 ++++++++++++++++++-- scripts/flaskext_compat.py | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 14c8d8d4..d030336f 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -47,7 +47,7 @@ class _ExtensionImporter(object): return self def load_module(self, fullname): - from sys import modules + from sys import modules, exc_info if fullname in modules: return modules[fullname] modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] @@ -56,12 +56,28 @@ class _ExtensionImporter(object): try: __import__(realname) except ImportError: + exc_type, exc_value, tb = exc_info() + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb continue module = modules[fullname] = modules[realname] if '.' not in modname: setattr(modules[__name__], modname, module) return module - raise ImportError(fullname) + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case + then we were able to import the module itself but apparently + something went wrong when the module was imported. (Eg: import + of an import failed). + """ + while tb is not None: + if tb.tb_frame.f_globals.get('__name__') == important_module: + return True + tb = tb.tb_next + return False _ExtensionImporter() diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index 03601a1a..f0b1739d 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -44,12 +44,28 @@ class _ExtensionImporter(object): try: __import__(realname) except ImportError: + exc_type, exc_value, tb = sys.exc_info() + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb continue module = sys.modules[fullname] = sys.modules[realname] if '.' not in modname: setattr(ext_module, modname, module) return module - raise ImportError(fullname) + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case + then we were able to import the module itself but apparently + something went wrong when the module was imported. (Eg: import + of an import failed). + """ + while tb is not None: + if tb.tb_frame.f_globals.get('__name__') == important_module: + return True + tb = tb.tb_next + return False def activate(): From ee9b4016323ba0f7a773a5e014bc23bcf1a322cd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Sep 2011 13:01:58 +0200 Subject: [PATCH 0848/3747] Added a comment on who the is_important_traceback seems to work. It's only speculation --- flask/ext/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index d030336f..86ad3da5 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -73,6 +73,20 @@ class _ExtensionImporter(object): something went wrong when the module was imported. (Eg: import of an import failed). """ + # Why can we access f_globals' __name__ here and the value is + # not None? I honestly don't know but here is my thinking. + # The module owns a reference to globals and the frame has one. + # Each function only keeps a reference to the globals not do the + # module which normally causes the problem that when the module + # shuts down all globals are set to None. Now however when the + # import system fails Python takes the short way out and does not + # actually properly shut down the module by Noneing the values + # but by just removing the entry from sys.modules. This means + # that the regular reference based cleanup kicks in. + # + # The good thing: At worst we will swallow an exception we should + # not and the error message will be messed up. However I think + # this should be sufficiently reliable. while tb is not None: if tb.tb_frame.f_globals.get('__name__') == important_module: return True From 9691b7f0bf70553e61a88c8c7b6ffcac776b8d26 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 21 Sep 2011 21:34:47 +0200 Subject: [PATCH 0849/3747] Deal with partially setup packages in the redirect hook. --- flask/ext/__init__.py | 11 ++++++++ flask/testsuite/__init__.py | 26 +++++++++++++++++++ flask/testsuite/ext.py | 7 +++++ .../test_apps/flask_broken/__init__.py | 2 ++ flask/testsuite/test_apps/flask_broken/b.py | 0 scripts/flaskext_compat.py | 10 +++---- 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 flask/testsuite/test_apps/flask_broken/__init__.py create mode 100644 flask/testsuite/test_apps/flask_broken/b.py diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 86ad3da5..1eda6df5 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -57,6 +57,17 @@ class _ExtensionImporter(object): __import__(realname) except ImportError: exc_type, exc_value, tb = exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + modules.pop(fullname, None) if self.is_important_traceback(realname, tb): raise exc_type, exc_value, tb continue diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 49b85b23..9f9e0a60 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -134,6 +134,32 @@ class FlaskTestCase(unittest.TestCase): def assert_equal(self, x, y): return self.assertEqual(x, y) + def assert_raises(self, exc_type, callable=None, *args, **kwargs): + catcher = _ExceptionCatcher(self, exc_type) + if callable is None: + return catcher + with catcher: + callable(*args, **kwargs) + + +class _ExceptionCatcher(object): + + def __init__(self, test_case, exc_type): + self.test_case = test_case + self.exc_type = exc_type + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + exception_name = self.exc_type.__name__ + if exc_type is None: + self.test_case.fail('Expected exception of type %r' % + exception_name) + elif not issubclass(exc_type, self.exc_type): + raise exc_type, exc_value, tb + return True + class BetterLoader(unittest.TestLoader): """A nicer loader that solves two problems. First of all we are setting diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index dc90952f..966db439 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -8,6 +8,8 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import sys import unittest from flask.testsuite import FlaskTestCase @@ -92,6 +94,11 @@ class ExtImportHookTestCase(FlaskTestCase): from flask.ext.oldext_package.submodule import test_function self.assert_equal(test_function(), 42) + def test_flaskext_broken_package_no_module_caching(self): + for x in xrange(2): + with self.assert_raises(ImportError): + import flask.ext.broken + def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/test_apps/flask_broken/__init__.py b/flask/testsuite/test_apps/flask_broken/__init__.py new file mode 100644 index 00000000..c194c04f --- /dev/null +++ b/flask/testsuite/test_apps/flask_broken/__init__.py @@ -0,0 +1,2 @@ +import flask.ext.broken.b +import missing_module diff --git a/flask/testsuite/test_apps/flask_broken/b.py b/flask/testsuite/test_apps/flask_broken/b.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index f0b1739d..bb3ada03 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -25,7 +25,8 @@ ext_module.__package__ = ext_module.__name__ class _ExtensionImporter(object): """This importer redirects imports from the flask.ext module to other - locations. + locations. For implementation details see the code in Flask 0.8 + that does the same. """ _module_choices = ['flask_%s', 'flaskext.%s'] prefix = ext_module.__name__ + '.' @@ -45,6 +46,7 @@ class _ExtensionImporter(object): __import__(realname) except ImportError: exc_type, exc_value, tb = sys.exc_info() + sys.modules.pop(fullname, None) if self.is_important_traceback(realname, tb): raise exc_type, exc_value, tb continue @@ -55,12 +57,6 @@ class _ExtensionImporter(object): raise ImportError('No module named %s' % fullname) def is_important_traceback(self, important_module, tb): - """Walks a traceback's frames and checks if any of the frames - originated in the given important module. If that is the case - then we were able to import the module itself but apparently - something went wrong when the module was imported. (Eg: import - of an import failed). - """ while tb is not None: if tb.tb_frame.f_globals.get('__name__') == important_module: return True From 0c75be1172000ad4d0b60920ef5a6fb78880a7d2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 22 Sep 2011 14:08:03 +0200 Subject: [PATCH 0850/3747] Whitespace normalize --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 255f1e77..b1424722 100644 --- a/setup.py +++ b/setup.py @@ -86,7 +86,7 @@ setup( 'and good intentions', long_description=__doc__, packages=['flask', 'flask.testsuite'], - include_package_data = True, + include_package_data=True, zip_safe=False, platforms='any', install_requires=[ From d4d75701bc1ea02a0e9b7e3e0d1aee777bd59116 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 22 Sep 2011 14:08:52 +0200 Subject: [PATCH 0851/3747] Added ext to the packages --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b1424722..64b24533 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ setup( description='A microframework based on Werkzeug, Jinja2 ' 'and good intentions', long_description=__doc__, - packages=['flask', 'flask.testsuite'], + packages=['flask', 'flask.ext', 'flask.testsuite'], include_package_data=True, zip_safe=False, platforms='any', From 56afafae67ff0db8b9994a4c59d978b56327e886 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 23 Sep 2011 02:04:21 +0200 Subject: [PATCH 0852/3747] Do not break extension tests if tested with installed extensions. --- flask/testsuite/__init__.py | 14 ++++++-------- flask/testsuite/ext.py | 7 ++++++- run-tests.py | 2 ++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 9f9e0a60..7ac7b536 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -24,7 +24,10 @@ from werkzeug.utils import import_string, find_modules def add_to_path(path): - """Adds an entry to sys.path_info if it's not already there.""" + """Adds an entry to sys.path_info if it's not already there. This does + not append it but moves it to the front so that we can be sure it + is loaded. + """ if not os.path.isdir(path): raise RuntimeError('Tried to add nonexisting path') @@ -33,13 +36,8 @@ def add_to_path(path): return os.path.samefile(x, y) except (IOError, OSError): return False - for entry in sys.path: - try: - if os.path.samefile(path, entry): - return - except (OSError, IOError): - pass - sys.path.append(path) + sys.path[:] = [x for x in sys.path if not _samefile(path, x)] + sys.path.insert(0, path) def iter_suites(): diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 966db439..c621bcf5 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -18,10 +18,15 @@ from flask.testsuite import FlaskTestCase class ExtImportHookTestCase(FlaskTestCase): def setup(self): + # we clear this out for various reasons. The most important one is + # that a real flaskext could be in there which would disable our + # fake package. Secondly we want to make sure that the flaskext + # import hook does not break on reloading. for entry, value in sys.modules.items(): if (entry.startswith('flask.ext.') or entry.startswith('flask_') or - entry.startswith('flaskext.')) and value is not None: + entry.startswith('flaskext.') or + entry == 'flaskext') and value is not None: sys.modules.pop(entry, None) from flask import ext reload(ext) diff --git a/run-tests.py b/run-tests.py index 4ef8a72d..c1345848 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,3 +1,5 @@ #!/usr/bin/env python +import sys, os +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) from flask.testsuite import main main() From b8ef7360eb80b2018c21384f1f32693ab22595d0 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 23 Sep 2011 10:34:41 +0300 Subject: [PATCH 0853/3747] Add to sys.path, not sys.path_info --- flask/testsuite/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 7ac7b536..76a4d724 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -24,7 +24,7 @@ from werkzeug.utils import import_string, find_modules def add_to_path(path): - """Adds an entry to sys.path_info if it's not already there. This does + """Adds an entry to sys.path if it's not already there. This does not append it but moves it to the front so that we can be sure it is loaded. """ From d2eefe25e7fec31a5f4254f6a65934bd00ab0392 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Sep 2011 20:25:28 +0200 Subject: [PATCH 0854/3747] app.name shall be __main__ for console apps --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 1ad669e3..adc017d0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -466,7 +466,7 @@ class Flask(_PackageBoundObject): if self.import_name == '__main__': fn = getattr(sys.modules['__main__'], '__file__', None) if fn is None: - return 'unknown' + return '__main__' return os.path.splitext(os.path.basename(fn))[0] return self.import_name From c6316132b173f96a6f5a90ff3b62f706f8a62560 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 24 Sep 2011 20:27:38 +0200 Subject: [PATCH 0855/3747] Context preserving is now part of Flask and not the test client. This fixes #326 --- CHANGES | 3 +++ flask/ctx.py | 35 ++++++++++++++++++++++++++++++++--- flask/globals.py | 1 + flask/testing.py | 29 +++++++++++------------------ flask/testsuite/basic.py | 17 +++++++++++++++++ 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/CHANGES b/CHANGES index 79a669f0..1801fc26 100644 --- a/CHANGES +++ b/CHANGES @@ -46,6 +46,9 @@ Relase date to be decided, codename to be chosen. - HEAD requests to a method view now automatically dispatch to the `get` method if no handler was implemented. - Implemented the virtual :mod:`flask.ext` package to import extensions from. +- The context preservation on exceptions is now an integral component of + Flask itself and no longer of the test client. This cleaned up some + internal logic and lowers the odds of runaway request contexts in unittests. Version 0.7.3 ------------- diff --git a/flask/ctx.py b/flask/ctx.py index 64293dc5..26781dbd 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -89,6 +89,10 @@ class RequestContext(object): self.flashes = None self.session = None + # indicator if the context was preserved. Next time another context + # is pushed the preserved context is popped. + self.preserved = False + self.match_request() # XXX: Support for deprecated functionality. This is doing away with @@ -114,6 +118,18 @@ class RequestContext(object): def push(self): """Binds the request context to the current context.""" + # If an exception ocurrs in debug mode or if context preservation is + # activated under exception situations exactly one context stays + # on the stack. The rationale is that you want to access that + # information under debug situations. However if someone forgets to + # pop that context again we want to make sure that on the next push + # it's invalidated otherwise we run at risk that something leaks + # memory. This is usually only a problem in testsuite since this + # functionality is not active in production environments. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() + _request_ctx_stack.push(self) # Open the session at the moment that the request context is @@ -128,8 +144,11 @@ class RequestContext(object): also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. """ + self.preserved = False self.app.do_teardown_request() - _request_ctx_stack.pop() + rv = _request_ctx_stack.pop() + assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ + % (rv, self) def __enter__(self): self.push() @@ -141,6 +160,16 @@ class RequestContext(object): # access the request object in the interactive shell. Furthermore # the context can be force kept alive for the test client. # See flask.testing for how this works. - if not self.request.environ.get('flask._preserve_context') and \ - (tb is None or not self.app.preserve_context_on_exception): + if self.request.environ.get('flask._preserve_context') or \ + (tb is not None and self.app.preserve_context_on_exception): + self.preserved = True + else: self.pop() + + def __repr__(self): + return '<%s \'%s\' [%s] of %s>' % ( + self.__class__.__name__, + self.request.url, + self.request.method, + self.app.name + ) diff --git a/flask/globals.py b/flask/globals.py index bcd08722..16580d16 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -19,6 +19,7 @@ def _lookup_object(name): raise RuntimeError('working outside of request context') return getattr(top, name) + # context locals _request_ctx_stack = LocalStack() current_app = LocalProxy(partial(_lookup_object, 'app')) diff --git a/flask/testing.py b/flask/testing.py index d2957036..782b40f6 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -37,7 +37,7 @@ class FlaskClient(Client): Basic usage is outlined in the :ref:`testing` chapter. """ - preserve_context = context_preserved = False + preserve_context = False @contextmanager def session_transaction(self, *args, **kwargs): @@ -88,7 +88,6 @@ class FlaskClient(Client): self.cookie_jar.extract_wsgi(c.request.environ, headers) def open(self, *args, **kwargs): - self._pop_reqctx_if_necessary() kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context @@ -97,14 +96,10 @@ class FlaskClient(Client): follow_redirects = kwargs.pop('follow_redirects', False) builder = make_test_environ_builder(self.application, *args, **kwargs) - old = _request_ctx_stack.top - try: - return Client.open(self, builder, - as_tuple=as_tuple, - buffered=buffered, - follow_redirects=follow_redirects) - finally: - self.context_preserved = _request_ctx_stack.top is not old + return Client.open(self, builder, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects) def __enter__(self): if self.preserve_context: @@ -114,12 +109,10 @@ class FlaskClient(Client): def __exit__(self, exc_type, exc_value, tb): self.preserve_context = False - self._pop_reqctx_if_necessary() - def _pop_reqctx_if_necessary(self): - if self.context_preserved: - # we have to use _request_ctx_stack.top.pop instead of - # _request_ctx_stack.pop since we want teardown handlers - # to be executed. - _request_ctx_stack.top.pop() - self.context_preserved = False + # on exit we want to clean up earlier. Normally the request context + # stays preserved until the next request in the same thread comes + # in. See RequestGlobals.push() for the general behavior. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 75286dbf..1733f0a3 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -904,6 +904,23 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assertEqual(c.get('/bar/').data, 'bar') self.assertEqual(c.get('/bar/123').data, '123') + def test_preserve_only_once(self): + app = flask.Flask(__name__) + app.debug = True + + @app.route('/fail') + def fail_func(): + 1/0 + + c = app.test_client() + for x in xrange(3): + with self.assert_raises(ZeroDivisionError): + c.get('/fail') + + self.assert_(flask._request_ctx_stack.top is not None) + flask._request_ctx_stack.pop() + self.assert_(flask._request_ctx_stack.top is None) + class ContextTestCase(FlaskTestCase): From df1dd57045b38ea7dcca44622744ab24e53e399e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 18:49:00 +0200 Subject: [PATCH 0856/3747] Cleaned up url routing common docs. This fixes #279 --- docs/api.rst | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ flask/app.py | 76 +++++++++------------------------------- 2 files changed, 114 insertions(+), 59 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index aca7fda5..5e5082bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -478,6 +478,103 @@ Class Based Views .. autoclass:: flask.views.MethodView :members: +.. _url-route-registrations: + +URL Route Registrations +----------------------- + +Generally there are three ways to define rules for the routing system: + +1. You can use the :meth:`flask.Flask.route` decorator. +2. You can use the :meth:`flask.Flask.add_url_rule` function. +3. You can directly access the underlying Werkzeug routing system + which is exposed as :attr:`flask.Flask.url_map`. + +Variables parts in the route can be specified with angular brackets +(``/user/``). By default a variable part in the URL accepts any +string without a slash however a different converter can be specified as +well by using ````. + +Variable parts are passed to the view function as keyword arguments. + +The following converters are possible available: + +=========== =============================================== +`unicode` accepts any text without a slash (the default) +`int` accepts integers +`float` like `int` but for floating point values +`path` like the default but also accepts slashes +=========== =============================================== + +Here some examples:: + + @app.route('/') + def index(): + pass + + @app.route('/') + def show_user(username): + pass + + @app.route('/post/') + def show_post(post_id): + pass + +An important detail to keep in mind is how Flask deals with trailing +slashes. The idea is to keep each URL unique so the following rules +apply: + +1. If a rule ends with a slash and is requested without a slash by the + user, the user is automatically redirected to the same page with a + trailing slash attached. +2. If a rule does not end with a trailing slash and the user request the + page with a trailing slash, a 404 not found is raised. + +This is consistent with how web servers deal with static files. This +also makes it possible to use relative link targets safely. + +You can also define multiple rules for the same function. They have to be +unique however. Defaults can also be specified. Here for example is a +definition for a URL that accepts an optional page:: + + @app.route('/users/', defaults={'page': 1}) + @app.route('/users/page/') + def show_users(page): + pass + +This specifies that ``/users/`` will be the URL for page one and +``/users/page/N`` will be the URL for page `N`. + +Here the parameters that :meth:`~flask.Flask.route` and +:meth:`~flask.Flask.add_url_rule` accept. The only difference is that +with the route parameter the view function is defined with the decorator +instead of the `view_func` parameter. + +=============== ========================================================== +`rule` the URL roule as string +`endpoint` the endpoint for the registered URL rule. Flask itself + assumes that the name of the view function is the name + of the endpoint if not explicitly stated. +`view_func` the function to call when serving a request to the + provided endpoint. If this is not provided one can + specify the function later by storing it in the + :attr:`~flask.Flask.view_functions` dictionary with the + endpoint as key. +`defaults` A dictionary with defaults for this rule. See the + example above for how defaults work. +`subdomain` specifies the rule for the subdomain in case subdomain + matching is in use. If not specified the default + subdomain is assumed. +`**options` the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change to + Werkzeug is handling of method options. methods is a list + of methods this rule should be limited to (`GET`, `POST` + etc.). By default a rule just listens for `GET` (and + implicitly `HEAD`). Starting with Flask 0.6, `OPTIONS` is + implicitly added and handled by the standard request + handling. They have to be specified as keyword arguments. +=============== ========================================================== + .. _view-func-options: View Function Options diff --git a/flask/app.py b/flask/app.py index adc017d0..91ea3414 100644 --- a/flask/app.py +++ b/flask/app.py @@ -824,9 +824,11 @@ class Flask(_PackageBoundObject): app.view_functions['index'] = index - If a view function is provided some defaults can be specified directly - on the view function. For more information refer to - :ref:`view-func-options`. + Internally :meth:`route` invokes :meth:`add_url_rule` so if you want + to customize the behavior via subclassing you only need to change + this method. + + For more information refer to :ref:`url-route-registrations`. .. versionchanged:: 0.2 `view_func` parameter added. @@ -885,73 +887,29 @@ class Flask(_PackageBoundObject): def route(self, rule, **options): """A decorator that is used to register a view function for a - given URL rule. Example:: + given URL rule. This does the same thing as :meth:`add_url_rule` + but is intended for decorator usage:: @app.route('/') def index(): return 'Hello World' - Variables parts in the route can be specified with angular - brackets (``/user/``). By default a variable part - in the URL accepts any string without a slash however a different - converter can be specified as well by using ````. - - Variable parts are passed to the view function as keyword - arguments. - - The following converters are possible: - - =========== =========================================== - `int` accepts integers - `float` like `int` but for floating point values - `path` like the default but also accepts slashes - =========== =========================================== - - Here some examples:: - - @app.route('/') - def index(): - pass - - @app.route('/') - def show_user(username): - pass - - @app.route('/post/') - def show_post(post_id): - pass - - An important detail to keep in mind is how Flask deals with trailing - slashes. The idea is to keep each URL unique so the following rules - apply: - - 1. If a rule ends with a slash and is requested without a slash - by the user, the user is automatically redirected to the same - page with a trailing slash attached. - 2. If a rule does not end with a trailing slash and the user request - the page with a trailing slash, a 404 not found is raised. - - This is consistent with how web servers deal with static files. This - also makes it possible to use relative link targets safely. - - The :meth:`route` decorator accepts a couple of other arguments - as well: + For more information refer to :ref:`url-route-registrations`. :param rule: the URL rule as string - :param methods: a list of methods this rule should be limited + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly `HEAD`). Starting with Flask 0.6, `OPTIONS` is implicitly added and handled by the standard request handling. - :param subdomain: specifies the rule for the subdomain in case - subdomain matching is in use. - :param strict_slashes: can be used to disable the strict slashes - setting for this rule. See above. - :param endpoint: Since version 0.8 you can also pass the enpoint, - it will be used instead of generating the endpoint - from the function name. - :param options: other options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. """ def decorator(f): endpoint = options.pop('endpoint', None) From 142c9c391be4ad182fb17d4190a19feea7b79246 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 18:56:15 +0200 Subject: [PATCH 0857/3747] Fixed typos in copy/pasted text --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5e5082bf..d80f5221 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -490,14 +490,14 @@ Generally there are three ways to define rules for the routing system: 3. You can directly access the underlying Werkzeug routing system which is exposed as :attr:`flask.Flask.url_map`. -Variables parts in the route can be specified with angular brackets +Variable parts in the route can be specified with angular brackets (``/user/``). By default a variable part in the URL accepts any string without a slash however a different converter can be specified as well by using ````. Variable parts are passed to the view function as keyword arguments. -The following converters are possible available: +The following converters are available: =========== =============================================== `unicode` accepts any text without a slash (the default) From 83b1da8df8ead2d419382af2ab3c3a4fa9db3b8b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 19:01:30 +0200 Subject: [PATCH 0858/3747] Inserted verb --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index d80f5221..59b99295 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -545,7 +545,7 @@ definition for a URL that accepts an optional page:: This specifies that ``/users/`` will be the URL for page one and ``/users/page/N`` will be the URL for page `N`. -Here the parameters that :meth:`~flask.Flask.route` and +Here are the parameters that :meth:`~flask.Flask.route` and :meth:`~flask.Flask.add_url_rule` accept. The only difference is that with the route parameter the view function is defined with the decorator instead of the `view_func` parameter. From 6d0b3264c2263e06eb0d5c854fd5782f04a34b24 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 19:02:47 +0200 Subject: [PATCH 0859/3747] Mention localhost:5000 being pointless in the docs. It already says that down on the page but not on the key --- docs/config.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 1ed004d2..75ddf86d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -90,7 +90,9 @@ The following configuration values are used internally by Flask: ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name and port number of the server. Required for subdomain support (e.g.: - ``'localhost:5000'``) + ``'myapp.dev:5000'``) Note that + localhost does not support subdomains so + setting this to “localhost” help. ``APPLICATION_ROOT`` If the application does not occupy a whole domain or subdomain this can be set to the path where the application From 6dccf775468ea0a849d3c1c3965e3a1db6a36a3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 19:12:41 +0200 Subject: [PATCH 0860/3747] PERMANENT_SESSION_LIFETIME can now be an integer. This fixes #310 --- docs/api.rst | 7 +++++++ docs/config.rst | 2 ++ flask/app.py | 35 ++++++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 59b99295..13fcb659 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -217,6 +217,13 @@ implementation that Flask is using. .. autoclass:: SessionMixin :members: +.. admonition:: Notice + + The ``PERMANENT_SESSION_LIFETIME`` config key can also be an integer + starting with Flask 0.8. Either catch this down yourself or use + the :attr:`~flask.Flask.permanent_session_lifetime` attribute on the + app which converts the result to an integer automatically. + Test Client ----------- diff --git a/docs/config.rst b/docs/config.rst index 75ddf86d..32e267c3 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -86,6 +86,8 @@ The following configuration values are used internally by Flask: `False`. ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as :class:`datetime.timedelta` object. + Starting with Flask 0.8 this can also be + an integer representing seconds. ``USE_X_SENDFILE`` enable/disable x-sendfile ``LOGGER_NAME`` the name of the logger ``SERVER_NAME`` the name and port number of the server. diff --git a/flask/app.py b/flask/app.py index 91ea3414..83721a6b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -177,15 +177,6 @@ class Flask(_PackageBoundObject): #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') - #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files #: sent with the :func:`send_file` method. @@ -495,6 +486,32 @@ class Flask(_PackageBoundObject): return rv return self.debug + def _get_permanent_session_lifetime(self): + """A :class:`~datetime.timedelta` which is used to set the expiration + date of a permanent session. The default is 31 days which makes a + permanent session survive for roughly one month. + + This attribute can also be configured from the config with the + `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + ``timedelta(days=31)``. + + If you want to have this value as seconds you can use ``total_seconds()``:: + + app.permanent_session_lifetime.total_seconds() + + Note that the config key can be a timedelta object or number of seconds + as integer since Flask 0.8. + """ + rv = self.config['PERMANENT_SESSION_LIFETIME'] + if not isinstance(rv, timedelta): + return timedelta(seconds=rv) + return rv + def _set_permanent_session_lifetime(self, value): + self.config['PERMANENT_SESSION_LIFETIME'] = value + permanent_session_lifetime = property(_get_permanent_session_lifetime, + _set_permanent_session_lifetime) + del _get_permanent_session_lifetime, _set_permanent_session_lifetime + @property def logger(self): """A :class:`logging.Logger` object for this application. The From 8da8a21b69cc06e58b1b28fa0ecec63b5cf3d9b4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 19:17:50 +0200 Subject: [PATCH 0861/3747] Moved the conversion thing into the ConfigAttribute. --- flask/app.py | 42 +++++++++++++++------------------------ flask/config.py | 8 ++++++-- flask/testsuite/config.py | 5 +++++ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/flask/app.py b/flask/app.py index 83721a6b..ebf4e6a6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -41,6 +41,12 @@ from .signals import request_started, request_finished, got_request_exception, \ _logger_lock = Lock() +def _make_timedelta(value): + if not isinstance(value, timedelta): + return timedelta(seconds=value) + return value + + def setupmethod(f): """Wraps a method so that it performs a check in debug mode if the first request was already handled. @@ -177,6 +183,16 @@ class Flask(_PackageBoundObject): #: `SESSION_COOKIE_NAME` configuration key. Defaults to ``'session'`` session_cookie_name = ConfigAttribute('SESSION_COOKIE_NAME') + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME', + get_converter=_make_timedelta) + #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files #: sent with the :func:`send_file` method. @@ -486,32 +502,6 @@ class Flask(_PackageBoundObject): return rv return self.debug - def _get_permanent_session_lifetime(self): - """A :class:`~datetime.timedelta` which is used to set the expiration - date of a permanent session. The default is 31 days which makes a - permanent session survive for roughly one month. - - This attribute can also be configured from the config with the - `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to - ``timedelta(days=31)``. - - If you want to have this value as seconds you can use ``total_seconds()``:: - - app.permanent_session_lifetime.total_seconds() - - Note that the config key can be a timedelta object or number of seconds - as integer since Flask 0.8. - """ - rv = self.config['PERMANENT_SESSION_LIFETIME'] - if not isinstance(rv, timedelta): - return timedelta(seconds=rv) - return rv - def _set_permanent_session_lifetime(self, value): - self.config['PERMANENT_SESSION_LIFETIME'] = value - permanent_session_lifetime = property(_get_permanent_session_lifetime, - _set_permanent_session_lifetime) - del _get_permanent_session_lifetime, _set_permanent_session_lifetime - @property def logger(self): """A :class:`logging.Logger` object for this application. The diff --git a/flask/config.py b/flask/config.py index 7b6cd1ee..67dbf9b7 100644 --- a/flask/config.py +++ b/flask/config.py @@ -21,13 +21,17 @@ from werkzeug.utils import import_string class ConfigAttribute(object): """Makes an attribute forward to the config""" - def __init__(self, name): + def __init__(self, name, get_converter=None): self.__name__ = name + self.get_converter = get_converter def __get__(self, obj, type=None): if obj is None: return self - return obj.config[self.__name__] + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv def __set__(self, obj, value): obj.config[self.__name__] = value diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 2ed726c8..1f6d79c3 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -78,6 +78,11 @@ class ConfigTestCase(FlaskTestCase): self.assert_(0, 'expected config') self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) + def test_session_lifetime(self): + app = flask.Flask(__name__) + app.config['PERMANENT_SESSION_LIFETIME'] = 42 + self.assert_equal(app.permanent_session_lifetime.total_seconds(), 42) + class InstanceTestCase(FlaskTestCase): From 363d9ed10dfa35816e4b635f9cc761925c8ef2ac Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 19:24:37 +0200 Subject: [PATCH 0862/3747] More doc typo fixes --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 13fcb659..7695788e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -513,7 +513,7 @@ The following converters are available: `path` like the default but also accepts slashes =========== =============================================== -Here some examples:: +Here are some examples:: @app.route('/') def index(): @@ -534,7 +534,7 @@ apply: 1. If a rule ends with a slash and is requested without a slash by the user, the user is automatically redirected to the same page with a trailing slash attached. -2. If a rule does not end with a trailing slash and the user request the +2. If a rule does not end with a trailing slash and the user requests the page with a trailing slash, a 404 not found is raised. This is consistent with how web servers deal with static files. This From 396c4bdcc0f30241e009516e430bc1742ad344bf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Sep 2011 22:19:20 +0200 Subject: [PATCH 0863/3747] Fixed a typo. "Does not" of course --- docs/config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 32e267c3..ca724dce 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -94,7 +94,8 @@ The following configuration values are used internally by Flask: Required for subdomain support (e.g.: ``'myapp.dev:5000'``) Note that localhost does not support subdomains so - setting this to “localhost” help. + setting this to “localhost” does not + help. ``APPLICATION_ROOT`` If the application does not occupy a whole domain or subdomain this can be set to the path where the application From f01b654ac4c0a4789369440557267b75df55fd59 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Sep 2011 12:27:33 +0200 Subject: [PATCH 0864/3747] total_seconds -> seconds for 2.6 and earlier --- flask/testsuite/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 1f6d79c3..ad1721fd 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -81,7 +81,7 @@ class ConfigTestCase(FlaskTestCase): def test_session_lifetime(self): app = flask.Flask(__name__) app.config['PERMANENT_SESSION_LIFETIME'] = 42 - self.assert_equal(app.permanent_session_lifetime.total_seconds(), 42) + self.assert_equal(app.permanent_session_lifetime.seconds, 42) class InstanceTestCase(FlaskTestCase): From 95c4dcb4d50e69b927855cd6a446579d2fe45b4e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Sep 2011 13:33:23 +0200 Subject: [PATCH 0865/3747] Refactored flask.ext process to not swallow exceptions on weird Pythons. --- flask/ext/__init__.py | 92 ++-------------------------- flask/exthook.py | 119 +++++++++++++++++++++++++++++++++++++ flask/testsuite/ext.py | 16 ++++- scripts/flaskext_compat.py | 93 ++++++++++++++++++++++------- 4 files changed, 211 insertions(+), 109 deletions(-) create mode 100644 flask/exthook.py diff --git a/flask/ext/__init__.py b/flask/ext/__init__.py index 1eda6df5..f29958a1 100644 --- a/flask/ext/__init__.py +++ b/flask/ext/__init__.py @@ -19,91 +19,11 @@ """ -class _ExtensionImporter(object): - """This importer redirects imports from this submodule to other locations. - This makes it possible to transition from the old flaskext.name to the - newer flask_name without people having a hard time. - """ - _module_choices = ['flask_%s', 'flaskext.%s'] - - def __init__(self): - from sys import meta_path - self.prefix = __name__ + '.' - self.prefix_cutoff = __name__.count('.') + 1 - - # since people might reload the flask.ext module (by accident or - # intentionally) we have to make sure to not add more than one - # import hook. We can't check class types here either since a new - # class will be created on reload. As a result of that we check - # the name of the class and remove stale instances. - def _name(x): - cls = type(x) - return cls.__module__ + '.' + cls.__name__ - this = _name(self) - meta_path[:] = [x for x in meta_path if _name(x) != this] + [self] - - def find_module(self, fullname, path=None): - if fullname.startswith(self.prefix): - return self - - def load_module(self, fullname): - from sys import modules, exc_info - if fullname in modules: - return modules[fullname] - modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] - for path in self._module_choices: - realname = path % modname - try: - __import__(realname) - except ImportError: - exc_type, exc_value, tb = exc_info() - # since we only establish the entry in sys.modules at the - # very this seems to be redundant, but if recursive imports - # happen we will call into the move import a second time. - # On the second invocation we still don't have an entry for - # fullname in sys.modules, but we will end up with the same - # fake module name and that import will succeed since this - # one already has a temporary entry in the modules dict. - # Since this one "succeeded" temporarily that second - # invocation now will have created a fullname entry in - # sys.modules which we have to kill. - modules.pop(fullname, None) - if self.is_important_traceback(realname, tb): - raise exc_type, exc_value, tb - continue - module = modules[fullname] = modules[realname] - if '.' not in modname: - setattr(modules[__name__], modname, module) - return module - raise ImportError('No module named %s' % fullname) - - def is_important_traceback(self, important_module, tb): - """Walks a traceback's frames and checks if any of the frames - originated in the given important module. If that is the case - then we were able to import the module itself but apparently - something went wrong when the module was imported. (Eg: import - of an import failed). - """ - # Why can we access f_globals' __name__ here and the value is - # not None? I honestly don't know but here is my thinking. - # The module owns a reference to globals and the frame has one. - # Each function only keeps a reference to the globals not do the - # module which normally causes the problem that when the module - # shuts down all globals are set to None. Now however when the - # import system fails Python takes the short way out and does not - # actually properly shut down the module by Noneing the values - # but by just removing the entry from sys.modules. This means - # that the regular reference based cleanup kicks in. - # - # The good thing: At worst we will swallow an exception we should - # not and the error message will be messed up. However I think - # this should be sufficiently reliable. - while tb is not None: - if tb.tb_frame.f_globals.get('__name__') == important_module: - return True - tb = tb.tb_next - return False +def setup(): + from ..exthook import ExtensionImporter + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) + importer.install() -_ExtensionImporter() -del _ExtensionImporter +setup() +del setup diff --git a/flask/exthook.py b/flask/exthook.py new file mode 100644 index 00000000..bb1deb29 --- /dev/null +++ b/flask/exthook.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" + flask.exthook + ~~~~~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + This is used by `flask.ext`. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os + + +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self.module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb.tb_next + continue + module = sys.modules[fullname] = sys.modules[realname] + if '.' not in modname: + setattr(sys.modules[self.wrapper_module], modname, module) + return module + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ + while tb is not None: + if self.is_important_frame(important_module, tb): + return True + tb = tb.tb_next + return False + + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index c621bcf5..034ab5be 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -35,8 +35,8 @@ class ExtImportHookTestCase(FlaskTestCase): import_hooks = 0 for item in sys.meta_path: cls = type(item) - if cls.__module__ == 'flask.ext' and \ - cls.__name__ == '_ExtensionImporter': + if cls.__module__ == 'flask.exthook' and \ + cls.__name__ == 'ExtensionImporter': import_hooks += 1 self.assert_equal(import_hooks, 1) @@ -104,6 +104,18 @@ class ExtImportHookTestCase(FlaskTestCase): with self.assert_raises(ImportError): import flask.ext.broken + def test_no_error_swallowing(self): + try: + import flask.ext.broken + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + self.assert_(exc_type is ImportError) + self.assert_equal(str(exc_value), 'No module named missing_module') + self.assert_(tb.tb_frame.f_globals is globals()) + + next = tb.tb_next + self.assert_('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) + def suite(): suite = unittest.TestSuite() diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index bb3ada03..40c8c6b5 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -15,22 +15,33 @@ :license: BSD, see LICENSE for more details. """ import sys +import os import imp -ext_module = imp.new_module('flask.ext') -ext_module.__path__ = [] -ext_module.__package__ = ext_module.__name__ - - -class _ExtensionImporter(object): - """This importer redirects imports from the flask.ext module to other - locations. For implementation details see the code in Flask 0.8 - that does the same. +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. """ - _module_choices = ['flask_%s', 'flaskext.%s'] - prefix = ext_module.__name__ + '.' - prefix_cutoff = prefix.count('.') + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] def find_module(self, fullname, path=None): if fullname.startswith(self.prefix): @@ -40,34 +51,74 @@ class _ExtensionImporter(object): if fullname in sys.modules: return sys.modules[fullname] modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] - for path in self._module_choices: + for path in self.module_choices: realname = path % modname try: __import__(realname) except ImportError: exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about if self.is_important_traceback(realname, tb): - raise exc_type, exc_value, tb + raise exc_type, exc_value, tb.tb_next continue module = sys.modules[fullname] = sys.modules[realname] if '.' not in modname: - setattr(ext_module, modname, module) + setattr(sys.modules[self.wrapper_module], modname, module) return module raise ImportError('No module named %s' % fullname) def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ while tb is not None: - if tb.tb_frame.f_globals.get('__name__') == important_module: + if self.is_important_frame(important_module, tb): return True tb = tb.tb_next return False + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename + def activate(): - """Activates the compatibility system.""" import flask - if hasattr(flask, 'ext'): - return - sys.modules['flask.ext'] = flask.ext = ext_module - sys.meta_path.append(_ExtensionImporter()) + ext_module = imp.new_module('flask.ext') + ext_module.__path__ = [] + flask.ext = sys.modules['flask.ext'] = ext_module + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], 'flask.ext') + importer.install() From 04b90f5ad47d04bace76c18f634d54b0da4b91ce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Sep 2011 14:36:06 +0200 Subject: [PATCH 0866/3747] Parentheses are for Python3 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 64b24533..14597afe 100644 --- a/setup.py +++ b/setup.py @@ -71,9 +71,9 @@ class run_audit(Command): if file != '__init__.py' and file.endswith('.py') : warns += flakes.checkPath(os.path.join(root, file)) if warns > 0: - print ("Audit finished with total %d warnings." % warns) + print "Audit finished with total %d warnings." % warns else: - print ("No problems found in sourcecode.") + print "No problems found in sourcecode." setup( name='Flask', From 585ff1db3db8569f16536aea75f64e1dd927041c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 29 Sep 2011 23:33:46 +0200 Subject: [PATCH 0867/3747] This will be Rakija --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 1801fc26..e1a3733a 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.8 ----------- -Relase date to be decided, codename to be chosen. +Released on September 29th 2011, codename Rakija - Refactored session support into a session interface so that the implementation of the sessions can be changed without From d5e10e4685f54dde5ffc27c4f55a19fb23f7a536 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 29 Sep 2011 23:34:02 +0200 Subject: [PATCH 0868/3747] Bump version number to 0.8 --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index c1076c33..73019aa8 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.8-dev' +__version__ = '0.8' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 14597afe..4e221bd9 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.8-dev', + version='0.8', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 3765cc2e9e6ea2d227f898d5b176aa50680e491c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 29 Sep 2011 23:36:57 +0200 Subject: [PATCH 0869/3747] This is 0.9-dev --- CHANGES | 5 +++++ flask/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index e1a3733a..d59a8b66 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.9 +----------- + +Relase date to be decided, codename to be chosen. + Version 0.8 ----------- diff --git a/flask/__init__.py b/flask/__init__.py index 73019aa8..54bfedda 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.8' +__version__ = '0.9-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 4e221bd9..d41a3eca 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.8', + version='0.9-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 766522cb587850f643f3bb6cb011735ae6a98510 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Oct 2011 01:06:22 +0200 Subject: [PATCH 0870/3747] Prepare for an 0.8.1 release --- CHANGES | 5 +++++ flask/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index e1a3733a..ba50d0d9 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.8.1 +------------- + +Bugfix release, release date to be decided + Version 0.8 ----------- diff --git a/flask/__init__.py b/flask/__init__.py index 73019aa8..04d7d1f2 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.8' +__version__ = '0.8.1-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 4e221bd9..af36a8bb 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.8', + version='0.8.1-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 0dd9dc37b6618b8091c2a0f849f5f3143dc6eafc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Oct 2011 01:08:54 +0200 Subject: [PATCH 0871/3747] Fixed an issue with an unused module for Python 2.5 (flask.session) --- CHANGES | 4 ++++ flask/session.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ba50d0d9..7640ac21 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,10 @@ Version 0.8.1 Bugfix release, release date to be decided +- Fixed an issue with the undocumented `flask.session` module to not + work properly on Python 2.5. It should not be used but did cause + some problems for package managers. + Version 0.8 ----------- diff --git a/flask/session.py b/flask/session.py index 4d4d2cd6..1a43fdc1 100644 --- a/flask/session.py +++ b/flask/session.py @@ -13,7 +13,7 @@ from warnings import warn warn(DeprecationWarning('please use flask.sessions instead')) -from .sessions import * +from .sessions import SecureCookieSession, NullSession Session = SecureCookieSession _NullSession = NullSession From b51fbdc8e00ea777dcfdfa39ae789206ecfdb0a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 6 Oct 2011 10:49:12 -0400 Subject: [PATCH 0872/3747] Removed a newline --- flask/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index be718cc6..f11c3ddd 100644 --- a/flask/views.py +++ b/flask/views.py @@ -15,7 +15,6 @@ http_method_funcs = frozenset(['get', 'post', 'head', 'options', 'delete', 'put', 'trace']) - class View(object): """Alternative way to use view functions. A subclass has to implement :meth:`dispatch_request` which is called with the view arguments from From 1759d8e4d80df3c3dc709c9bace2eb07d5685596 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 6 Oct 2011 10:57:03 -0400 Subject: [PATCH 0873/3747] Added support for anchor link generation. --- CHANGES | 3 +++ flask/helpers.py | 11 ++++++++++- flask/testsuite/helpers.py | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index a1b9fedc..76ac8a22 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,9 @@ Version 0.9 Relase date to be decided, codename to be chosen. +- The :func:`flask.url_for` function now can generate anchors to the + generated links. + Version 0.8.1 ------------- diff --git a/flask/helpers.py b/flask/helpers.py index 72c8f170..2471103d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -18,6 +18,7 @@ import mimetypes from time import time from zlib import adler32 from threading import RLock +from werkzeug.urls import url_quote # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -184,9 +185,13 @@ def url_for(endpoint, **values): For more information, head over to the :ref:`Quickstart `. + .. versionadded:: 0.9 + The `_anchor` parameter was added. + :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. + :param _anchor: if provided this is added as anchor to the URL. """ ctx = _request_ctx_stack.top blueprint_name = request.blueprint @@ -204,8 +209,12 @@ def url_for(endpoint, **values): elif endpoint.startswith('.'): endpoint = endpoint[1:] external = values.pop('_external', False) + anchor = values.pop('_anchor', None) ctx.app.inject_url_defaults(endpoint, values) - return ctx.url_adapter.build(endpoint, values, force_external=external) + rv = ctx.url_adapter.build(endpoint, values, force_external=external) + if anchor is not None: + rv += '#' + url_quote(anchor) + return rv def get_template_attribute(template_name, attribute): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 052d36e1..80729821 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -288,6 +288,15 @@ class LoggingTestCase(FlaskTestCase): self.assert_equal(rv.status_code, 500) self.assert_equal(rv.data, 'Hello Server Error') + def test_url_for_with_anchor(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return '42' + with app.test_request_context(): + self.assert_equal(flask.url_for('index', _anchor='x y'), + '/#x%20y') + def suite(): suite = unittest.TestSuite() From b115b38dbec3598735c57e1f48610cd82b161af1 Mon Sep 17 00:00:00 2001 From: kracekumar Date: Thu, 6 Oct 2011 23:45:35 +0530 Subject: [PATCH 0874/3747] Added PasswordField in docs/patterns/wtforms.rst --- docs/patterns/wtforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 93824df7..ed530427 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -26,7 +26,7 @@ The Forms This is an example form for a typical registration page:: - from wtforms import Form, BooleanField, TextField, validators + from wtforms import Form, BooleanField, TextField, PasswordField, validators class RegistrationForm(Form): username = TextField('Username', [validators.Length(min=4, max=25)]) From ebd74688070617a4cb6dd39df616f4691a98666d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 Oct 2011 15:09:02 -0400 Subject: [PATCH 0875/3747] Added a newline --- flask/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/helpers.py b/flask/helpers.py index 2471103d..62b6a3a4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -56,6 +56,7 @@ def _assert_have_json(): if not json_available: raise RuntimeError('simplejson not installed') + # figure out if simplejson escapes slashes. This behaviour was changed # from one version to another without reason. if not json_available or '\\/' not in json.dumps('/'): From 230e136f32746c5dbd4e06dbad3e329807440c71 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 Oct 2011 15:09:12 -0400 Subject: [PATCH 0876/3747] Subscribe to signals with extra kwargs in the docs --- docs/signals.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 0d1d9eea..c381da92 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -86,7 +86,7 @@ specified that way one has to pass the list in as argument:: from flask import template_rendered - def captured_templates(app, recorded): + def captured_templates(app, recorded, **extra): def record(sender, template, context): recorded.append((template, context)) return template_rendered.connected_to(record, app) @@ -94,7 +94,7 @@ specified that way one has to pass the list in as argument:: The example above would then look like this:: templates = [] - with captured_templates(app, templates): + with captured_templates(app, templates, **extra): ... template, context = templates[0] @@ -162,7 +162,7 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new from flask import template_rendered @template_rendered.connect_via(app) - def when_template_rendered(sender, template, context): + def when_template_rendered(sender, template, context, **extra): print 'Template %s is rendered with %s' % (template.name, context) Core Signals @@ -181,7 +181,7 @@ The following signals exist in Flask: Example subscriber:: - def log_template_renders(sender, template, context): + def log_template_renders(sender, template, context, **extra): sender.logger.debug('Rendering template "%s" with context %s', template.name or 'string template', context) @@ -199,7 +199,7 @@ The following signals exist in Flask: Example subscriber:: - def log_request(sender): + def log_request(sender, **extra): sender.logger.debug('Request context is set up') from flask import request_started @@ -213,7 +213,7 @@ The following signals exist in Flask: Example subscriber:: - def log_response(sender, response): + def log_response(sender, response, **extra): sender.logger.debug('Request context is about to close down. ' 'Response: %s', response) @@ -230,7 +230,7 @@ The following signals exist in Flask: Example subscriber:: - def log_exception(sender, exception): + def log_exception(sender, exception, **extra): sender.logger.debug('Got exception during processing: %s', exception) from flask import got_request_exception @@ -246,7 +246,7 @@ The following signals exist in Flask: Example subscriber:: - def close_db_connection(sender): + def close_db_connection(sender, **extra): session.close() from flask import request_tearing_down From 174f32250ec54950badbaf3878d9d6acc8328ddd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 Oct 2011 15:19:03 -0400 Subject: [PATCH 0877/3747] Mention that people subscribe with **extra. --- docs/signals.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index c381da92..75487800 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -55,7 +55,7 @@ to the template:: @contextmanager def captured_templates(app): recorded = [] - def record(sender, template, context): + def record(sender, template, context, **extra): recorded.append((template, context)) template_rendered.connect(record, app) try: @@ -73,6 +73,9 @@ This can now easily be paired with a test client:: assert template.name == 'index.html' assert len(context['items']) == 10 +Make sure to subscribe with an extra ``**extra`` argument so that your +calls don't fail if Flask introduces new arguments to the signals. + All the template rendering in the code issued by the application `app` in the body of the `with` block will now be recorded in the `templates` variable. Whenever a template is rendered, the template object as well as From 61a95196ac28771466ada43a1c01effda42040a5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 11 Oct 2011 19:09:37 -0700 Subject: [PATCH 0878/3747] Changed logic for debug level log settings --- CHANGES | 2 ++ flask/logging.py | 4 +++- flask/testsuite/helpers.py | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 76ac8a22..aa7fcf34 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ Relase date to be decided, codename to be chosen. - The :func:`flask.url_for` function now can generate anchors to the generated links. +- Logger now only returns the debug log setting if it was not set + explicitly. Version 0.8.1 ------------- diff --git a/flask/logging.py b/flask/logging.py index b992aef8..9ad641d1 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -25,7 +25,9 @@ def create_logger(app): class DebugLogger(Logger): def getEffectiveLevel(x): - return DEBUG if app.debug else Logger.getEffectiveLevel(x) + if x.level == 0 and app.debug: + return DEBUG + return Logger.getEffectiveLevel(x) class DebugHandler(StreamHandler): def emit(x, record): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 80729821..06b0542d 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -246,6 +246,13 @@ class LoggingTestCase(FlaskTestCase): else: self.assert_(False, 'debug log ate the exception') + def test_debug_log_override(self): + app = flask.Flask(__name__) + app.debug = True + app.logger_name = 'flask_tests/test_debug_log_override' + app.logger.level = 10 + self.assert_equal(app.logger.level, 10) + def test_exception_logging(self): out = StringIO() app = flask.Flask(__name__) From 4ab97047dee359af1afce6fe5431ffa7c5ed5795 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 13 Oct 2011 11:49:51 +0300 Subject: [PATCH 0879/3747] Typo fix --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index aa7fcf34..6dd66b09 100644 --- a/CHANGES +++ b/CHANGES @@ -46,7 +46,7 @@ Released on September 29th 2011, codename Rakija of a value error which usually would result in a 500 internal server error if not handled. This is a backwards incompatible change. - Applications now not only have a root path where the resources and modules - are located but also an instane path which is the designated place to + are located but also an instance path which is the designated place to drop files that are modified at runtime (uploads etc.). Also this is conceptionally only instance depending and outside version control so it's the perfect place to put configuration files etc. For more information From 11c9bf29437ce847089703876cdec8cea68ca594 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 17 Oct 2011 13:12:12 +0000 Subject: [PATCH 0880/3747] Api fix status_code => status --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 7695788e..47ff84e5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -144,7 +144,7 @@ Response Objects A :class:`Headers` object representing the response headers. - .. attribute:: status_code + .. attribute:: status The response status as integer. From e4d9ccd6ec889a8b52b28953c0bb6d490db7df63 Mon Sep 17 00:00:00 2001 From: Cory Li Date: Wed, 2 Nov 2011 01:45:39 -0300 Subject: [PATCH 0881/3747] Changing instance_root to instance_path --- docs/config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index ca724dce..2f9d8307 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -375,7 +375,7 @@ file from the instance folder with :meth:`Flask.open_instance_resource`. Example usage for both:: - filename = os.path.join(app.instance_root, 'application.cfg') + filename = os.path.join(app.instance_path, 'application.cfg') with open(filename) as f: config = f.read() From f52e7a9dc944f425c2f2a77706bc2af98b23295c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 4 Nov 2011 02:46:22 +0100 Subject: [PATCH 0882/3747] Added support for _method to url_for() --- CHANGES | 2 ++ flask/helpers.py | 7 +++++-- flask/testsuite/helpers.py | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index aa7fcf34..3562bf90 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ Relase date to be decided, codename to be chosen. - The :func:`flask.url_for` function now can generate anchors to the generated links. +- The :func:`flask.url_for` function now can also explicitly generate + URL rules specific to a given HTTP method. - Logger now only returns the debug log setting if it was not set explicitly. diff --git a/flask/helpers.py b/flask/helpers.py index 62b6a3a4..7295dc3c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -187,12 +187,13 @@ def url_for(endpoint, **values): For more information, head over to the :ref:`Quickstart `. .. versionadded:: 0.9 - The `_anchor` parameter was added. + The `_anchor` and `_method` parameters were added. :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. :param _anchor: if provided this is added as anchor to the URL. + :param _method: if provided this explicitly specifies an HTTP method. """ ctx = _request_ctx_stack.top blueprint_name = request.blueprint @@ -211,8 +212,10 @@ def url_for(endpoint, **values): endpoint = endpoint[1:] external = values.pop('_external', False) anchor = values.pop('_anchor', None) + method = values.pop('_method', None) ctx.app.inject_url_defaults(endpoint, values) - rv = ctx.url_adapter.build(endpoint, values, force_external=external) + rv = ctx.url_adapter.build(endpoint, values, method=method, + force_external=external) if anchor is not None: rv += '#' + url_quote(anchor) return rv diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 06b0542d..41e31be9 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -304,6 +304,32 @@ class LoggingTestCase(FlaskTestCase): self.assert_equal(flask.url_for('index', _anchor='x y'), '/#x%20y') + def test_url_with_method(self): + from flask.views import MethodView + app = flask.Flask(__name__) + class MyView(MethodView): + def get(self, id=None): + if id is None: + return 'List' + return 'Get %d' % id + def post(self): + return 'Create' + myview = MyView.as_view('myview') + app.add_url_rule('/myview/', methods=['GET'], + view_func=myview) + app.add_url_rule('/myview/', methods=['GET'], + view_func=myview) + app.add_url_rule('/myview/create', methods=['POST'], + view_func=myview) + + with app.test_request_context(): + self.assert_equal(flask.url_for('myview', _method='GET'), + '/myview/') + self.assert_equal(flask.url_for('myview', id=42, _method='GET'), + '/myview/42') + self.assert_equal(flask.url_for('myview', _method='POST'), + '/myview/create') + def suite(): suite = unittest.TestSuite() From 51e4a58a85bc1500d1400ba7d633d570f6296745 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Sat, 5 Nov 2011 09:02:05 -0400 Subject: [PATCH 0883/3747] Fix flask issue #338 Give POST its own url_rule to avoid TypeError. --- docs/views.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/views.rst b/docs/views.rst index 441620a6..37904729 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -210,7 +210,8 @@ and explicitly mentioning the methods for each:: user_view = UserAPI.as_view('user_api') app.add_url_rule('/users/', defaults={'user_id': None}, - view_func=user_view, methods=['GET', 'POST']) + view_func=user_view, methods=['GET',]) + app.add_url_rule('/users/', view_func=user_view, methods=['POST',]) app.add_url_rule('/users/', view_func=user_view, methods=['GET', 'PUT', 'DELETE']) @@ -220,7 +221,8 @@ registration code:: def register_api(view, endpoint, url, pk='id', pk_type='int'): view_func = view.as_view(endpoint) app.add_url_rule(url, defaults={pk: None}, - view_func=view_func, methods=['GET', 'POST']) + view_func=view_func, methods=['GET',]) + app.add_url_rule(url, view_func=view_func, methods=['POST',]) app.add_url_rule('%s<%s:%s>' % (url, pk), view_func=view_func, methods=['GET', 'PUT', 'DELETE']) From e345a3afb5bf6ebf20e566f0846a58d6f55971eb Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Sat, 5 Nov 2011 09:03:18 -0400 Subject: [PATCH 0884/3747] Fix string format error while we're in here. --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index 37904729..a48d81f2 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -223,7 +223,7 @@ registration code:: app.add_url_rule(url, defaults={pk: None}, view_func=view_func, methods=['GET',]) app.add_url_rule(url, view_func=view_func, methods=['POST',]) - app.add_url_rule('%s<%s:%s>' % (url, pk), view_func=view_func, + app.add_url_rule('%s<%s:%s>' % (url, pk, pk_type), view_func=view_func, methods=['GET', 'PUT', 'DELETE']) register_api(UserAPI, 'user_api', '/users/', pk='user_id') From 7f4c12b33565c4d0802c4385bb2bbaa6c5735196 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 5 Nov 2011 17:43:40 +0100 Subject: [PATCH 0885/3747] Break up a circular dependency on shutdown --- CHANGES | 6 +++ flask/ctx.py | 4 ++ flask/testsuite/regression.py | 83 +++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 flask/testsuite/regression.py diff --git a/CHANGES b/CHANGES index ff6bdef2..e30979d8 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,12 @@ Relase date to be decided, codename to be chosen. URL rules specific to a given HTTP method. - Logger now only returns the debug log setting if it was not set explicitly. +- Unregister a circular dependency between the WSGI environment and + the request object when shutting down the request. This means that + environ ``werkzeug.request`` will be `None` after the response was + returned to the WSGI server but has the advantage that the garbage + collector is not needed on CPython to tear down the request unless + the user created circular dependencies themselves. Version 0.8.1 ------------- diff --git a/flask/ctx.py b/flask/ctx.py index 26781dbd..9a72d251 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -150,6 +150,10 @@ class RequestContext(object): assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ % (rv, self) + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + rv.request.environ['werkzeug.request'] = None + def __enter__(self): self.push() return self diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py new file mode 100644 index 00000000..51a866a4 --- /dev/null +++ b/flask/testsuite/regression.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.regression + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests regressions. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import gc +import sys +import flask +import threading +import unittest +from werkzeug.test import run_wsgi_app, create_environ +from flask.testsuite import FlaskTestCase + + +_gc_lock = threading.Lock() + + +class _NoLeakAsserter(object): + + def __init__(self, testcase): + self.testcase = testcase + + def __enter__(self): + gc.disable() + _gc_lock.acquire() + loc = flask._request_ctx_stack._local + + # Force Python to track this dictionary at all times. + # This is necessary since Python only starts tracking + # dicts if they contain mutable objects. It's a horrible, + # horrible hack but makes this kinda testable. + loc.__storage__['FOOO'] = [1, 2, 3] + + gc.collect() + self.old_objects = len(gc.get_objects()) + + def __exit__(self, exc_type, exc_value, tb): + if not hasattr(sys, 'getrefcount'): + gc.collect() + new_objects = len(gc.get_objects()) + if new_objects > self.old_objects: + self.testcase.fail('Example code leaked') + _gc_lock.release() + gc.enable() + + +class MemoryTestCase(FlaskTestCase): + + def assert_no_leak(self): + return _NoLeakAsserter(self) + + def test_memory_consumption(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('simple_template.html', whiskey=42) + + def fire(): + with app.test_client() as c: + rv = c.get('/') + self.assert_equal(rv.status_code, 200) + self.assert_equal(rv.data, '

    42

    ') + + # Trigger caches + fire() + + with self.assert_no_leak(): + for x in xrange(10): + fire() + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(MemoryTestCase)) + return suite From 9dd61eea6b49bfe1cc4639f5bc4c1371df938d95 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Tue, 8 Nov 2011 10:31:45 +0200 Subject: [PATCH 0886/3747] Added testcase for redirect and session keeparound bug --- flask/testsuite/testing.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 6574e77d..f9ee909b 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -46,6 +46,32 @@ class TestToolsTestCase(FlaskTestCase): rv = c.get('/') self.assert_equal(rv.data, 'http://localhost/') + def test_redirect_keep_session(self): + app = flask.Flask(__name__) + app.secret_key = 'testing' + + @app.route('/', methods=['GET', 'POST']) + def index(): + if flask.request.method == 'POST': + return flask.redirect('/redirect') + flask.session['data'] = 'foo' + return 'index' + + @app.route('/redirect') + def redirect(): + return 'redirect' + + with app.test_client() as c: + ctx = app.test_request_context() + ctx.push() + rv = c.get('/') + assert rv.data == 'index' + assert flask.session.get('data') == 'foo' + rv = c.post('/', data={}, follow_redirects=True) + assert rv.data == 'redirect' + assert flask.session.get('data') == 'foo' + ctx.pop() + def test_session_transactions(self): app = flask.Flask(__name__) app.testing = True From d628df6ab6c57b34acfb412e135d0d095636c539 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 20 Nov 2011 16:54:40 +0100 Subject: [PATCH 0887/3747] Store session after callbacks. This fixes #351 --- CHANGES | 3 +++ flask/app.py | 4 ++-- flask/testsuite/basic.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index e30979d8..61723dca 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,9 @@ Relase date to be decided, codename to be chosen. returned to the WSGI server but has the advantage that the garbage collector is not needed on CPython to tear down the request unless the user created circular dependencies themselves. +- Session is now stored after callbacks so that if the session payload + is stored in the session you can still modify it in an after + request callback. Version 0.8.1 ------------- diff --git a/flask/app.py b/flask/app.py index ebf4e6a6..42ffea4d 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1403,8 +1403,6 @@ class Flask(_PackageBoundObject): """ ctx = _request_ctx_stack.top bp = ctx.request.blueprint - if not self.session_interface.is_null_session(ctx.session): - self.save_session(ctx.session, response) funcs = () if bp is not None and bp in self.after_request_funcs: funcs = reversed(self.after_request_funcs[bp]) @@ -1412,6 +1410,8 @@ class Flask(_PackageBoundObject): funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) + if not self.session_interface.is_null_session(ctx.session): + self.save_session(ctx.session, response) return response def do_teardown_request(self): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 1733f0a3..a11f7806 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -279,6 +279,23 @@ class BasicFunctionalityTestCase(FlaskTestCase): match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) self.assert_(match is None) + def test_session_stored_last(self): + app = flask.Flask(__name__) + app.secret_key = 'development-key' + app.testing = True + + @app.after_request + def modify_session(response): + flask.session['foo'] = 42 + return response + @app.route('/') + def dump_session_contents(): + return repr(flask.session.get('foo')) + + c = app.test_client() + self.assert_equal(c.get('/').data, 'None') + self.assert_equal(c.get('/').data, '42') + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From 75050d4bc5ee656c7b33d998107829b6ff138f6d Mon Sep 17 00:00:00 2001 From: Craig Dennis Date: Sun, 20 Nov 2011 17:03:37 +0100 Subject: [PATCH 0888/3747] Simple documentation corrections, mostly typos. --- docs/foreword.rst | 2 +- docs/quickstart.rst | 2 +- docs/signals.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 10b886bf..0f649588 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -44,7 +44,7 @@ object relational mappers, form validation, upload handling, various open authentication technologies and more. Since Flask is based on a very solid foundation there is not a lot of code -in Flask itself. As such it's easy to adapt even for lage applications +in Flask itself. As such it's easy to adapt even for large applications and we are making sure that you can either configure it as much as possible by subclassing things or by forking the entire codebase. If you are interested in that, check out the :ref:`becomingbig` chapter. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 34aa3be4..df97d5ad 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -616,7 +616,7 @@ Storing cookies:: resp.set_cookie('username', 'the username') return resp -Note that cookies are set on response objects. Since you normally you +Note that cookies are set on response objects. Since you normally just return strings from the view functions Flask will convert them into response objects for you. If you explicitly want to do that you can use the :meth:`~flask.make_response` function and then modify it. diff --git a/docs/signals.rst b/docs/signals.rst index 75487800..2d3878f7 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -83,7 +83,7 @@ context are appended to it. Additionally there is a convenient helper method (:meth:`~blinker.base.Signal.connected_to`). that allows you to -temporarily subscribe a function to a signal with is a context manager on +temporarily subscribe a function to a signal with a context manager on its own. Because the return value of the context manager cannot be specified that way one has to pass the list in as argument:: From 9c8f138a42aa43ee8177ed84df5946f8ef6d0c58 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 25 Nov 2011 21:08:19 +0100 Subject: [PATCH 0889/3747] Improved test coverage for the test client --- flask/testsuite/testing.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index f9ee909b..1170b306 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -53,24 +53,32 @@ class TestToolsTestCase(FlaskTestCase): @app.route('/', methods=['GET', 'POST']) def index(): if flask.request.method == 'POST': - return flask.redirect('/redirect') + return flask.redirect('/getsession') flask.session['data'] = 'foo' return 'index' - @app.route('/redirect') - def redirect(): - return 'redirect' + @app.route('/getsession') + def get_session(): + return flask.session.get('data', '') with app.test_client() as c: - ctx = app.test_request_context() - ctx.push() + rv = c.get('/getsession') + assert rv.data == '' + rv = c.get('/') assert rv.data == 'index' assert flask.session.get('data') == 'foo' rv = c.post('/', data={}, follow_redirects=True) - assert rv.data == 'redirect' - assert flask.session.get('data') == 'foo' - ctx.pop() + assert rv.data == 'foo' + + # XXX: Currently the test client does not support + # keeping the context around if a redirect is followed. + # This would be nice to fix but right now the Werkzeug + # test client does not support that. + ##assert flask.session.get('data') == 'foo' + + rv = c.get('/getsession') + assert rv.data == 'foo' def test_session_transactions(self): app = flask.Flask(__name__) @@ -113,6 +121,7 @@ class TestToolsTestCase(FlaskTestCase): with app.test_client() as c: rv = c.get('/') req = flask.request._get_current_object() + self.assert_(req is not None) with c.session_transaction(): self.assert_(req is flask.request._get_current_object()) From 4fe80c05c6f3fc24f145f34e2bc6ce7fbe9f21ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Nov 2011 22:47:36 +0100 Subject: [PATCH 0890/3747] Not yet stable? Bollocks --- docs/_templates/sidebarintro.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 164c8745..850fe86a 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -1,9 +1,7 @@

    About Flask

    Flask is a micro webdevelopment framework for Python. You are currently - looking at the documentation of the development version. Things are - not stable yet, but if you have some feedback, - let me know. + looking at the documentation of the development version.

    Other Formats

    From c90858a95d5fbe474ba075c3221de0b6f38c9ef2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 28 Nov 2011 22:48:14 +0100 Subject: [PATCH 0891/3747] Added a branch to test functionality enabled by new test client --- flask/testsuite/testing.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 1170b306..0e6feb60 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -71,11 +71,9 @@ class TestToolsTestCase(FlaskTestCase): rv = c.post('/', data={}, follow_redirects=True) assert rv.data == 'foo' - # XXX: Currently the test client does not support - # keeping the context around if a redirect is followed. - # This would be nice to fix but right now the Werkzeug - # test client does not support that. - ##assert flask.session.get('data') == 'foo' + # This support requires a new Werkzeug version + if not hasattr(c, 'redirect_client'): + assert flask.session.get('data') == 'foo' rv = c.get('/getsession') assert rv.data == 'foo' From a9726c43aca2cf7daed3b3690b060494911e72c5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Dec 2011 18:59:37 -0500 Subject: [PATCH 0892/3747] Updated mod_wsgi docs to reference the sys.path hackery --- docs/deploying/mod_wsgi.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index c85ed64f..c4cd3d61 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -61,7 +61,12 @@ Store that file somewhere that you will find it again (e.g.: `/var/www/yourapplication`) and make sure that `yourapplication` and all the libraries that are in use are on the python load path. If you don't want to install it system wide consider using a `virtual python`_ -instance. +instance. Keep in mind that you will have to actually install your +application into the virtualenv as well. Alternatively there is the +option to just patch the path in the `.wsgi` file before the import:: + + import sys + sys.path.insert(0, '/path/to/the/application') Configuring Apache ------------------ From 7ccca13bbdfcbab126722c050dc4ea3d8fa56dc3 Mon Sep 17 00:00:00 2001 From: FND Date: Sat, 24 Dec 2011 10:55:52 +0100 Subject: [PATCH 0893/3747] Fixed various stylistic issues in documentation --- docs/foreword.rst | 28 +++++------ docs/installation.rst | 18 +++---- docs/quickstart.rst | 113 +++++++++++++++++++++--------------------- 3 files changed, 79 insertions(+), 80 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 0f649588..7678d014 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -9,16 +9,16 @@ What does "micro" mean? ----------------------- To me, the "micro" in microframework refers not only to the simplicity and -small size of the framework, but also the fact that it does not make much +small size of the framework, but also the fact that it does not make many decisions for you. While Flask does pick a templating engine for you, we won't make such decisions for your datastore or other parts. -For us however the term “micro” does not mean that the whole implementation +However, to us the term “micro” does not mean that the whole implementation has to fit into a single Python file. One of the design decisions with Flask was that simple tasks should be -simple and not take up a lot of code and yet not limit yourself. Because -of that we took a few design choices that some people might find +simple; they should not take a lot of code and yet they should not limit you. +Because of that we made a few design choices that some people might find surprising or unorthodox. For example, Flask uses thread-local objects internally so that you don't have to pass objects around from function to function within a request in order to stay threadsafe. While this is a @@ -30,17 +30,17 @@ and provide you with a lot of tools to make it as pleasant as possible to work with them. Flask is also based on convention over configuration, which means that -many things are preconfigured. For example, by convention, templates and -static files are in subdirectories within the Python source tree of the -application. While this can be changed you usually don't have to. +many things are preconfigured. For example, by convention templates and +static files are stored in subdirectories within the application's Python source tree. +While this can be changed you usually don't have to. -The main reason however why Flask is called a "microframework" is the idea +The main reason Flask is called a "microframework" is the idea to keep the core simple but extensible. There is no database abstraction layer, no form validation or anything else where different libraries -already exist that can handle that. However Flask knows the concept of -extensions that can add this functionality into your application as if it +already exist that can handle that. However Flask supports +extensions to add such functionality to your application as if it was implemented in Flask itself. There are currently extensions for -object relational mappers, form validation, upload handling, various open +object-relational mappers, form validation, upload handling, various open authentication technologies and more. Since Flask is based on a very solid foundation there is not a lot of code @@ -71,7 +71,7 @@ cause security problems. The documentation will warn you about aspects of web development that require attention to security. Some of these security concerns are far more complex than one might think, and we all sometimes underestimate -the likelihood that a vulnerability will be exploited, until a clever +the likelihood that a vulnerability will be exploited - until a clever attacker figures out a way to exploit our applications. And don't think that your application is not important enough to attract an attacker. Depending on the kind of attack, chances are that automated bots are @@ -88,8 +88,8 @@ support the new iteration of the Python programming language. While the situation is greatly improving there are still some issues that make it hard for us to switch over to Python 3 just now. These problems are partially caused by changes in the language that went unreviewed for too -long, partially also because we have not quite worked out how the lower -level API should change for the unicode differences in Python3. +long, partially also because we have not quite worked out how the lower- +level API should change to account for the Unicode differences in Python 3. Werkzeug and Flask will be ported to Python 3 as soon as a solution for the changes is found, and we will provide helpful tips how to upgrade diff --git a/docs/installation.rst b/docs/installation.rst index eb645bdc..55065b6d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,8 +9,8 @@ Werkzeug is a toolkit for WSGI, the standard Python interface between web applications and a variety of servers for both development and deployment. Jinja2 renders templates. -So how do you get all that on your computer quickly? There are many ways -which this section will explain, but the most kick-ass method is +So how do you get all that on your computer quickly? There are many ways, +as this section will explain, but the most kick-ass method is virtualenv, so let's look at that first. Either way, you will need Python 2.5 or higher to get started, so be sure @@ -58,7 +58,7 @@ even in your package manager. If you use Ubuntu, try:: If you are on Windows and don't have the `easy_install` command, you must install it first. Check the :ref:`windows-easy-install` section for more -information about how to do that. Once you have it installed, run the +information on how to do that. Once you have it installed, run the same commands as above, but without the `sudo` prefix. Once you have virtualenv installed, just fire up a shell and create @@ -78,7 +78,7 @@ the corresponding environment. On OS X and Linux, do the following:: (Note the space between the dot and the script name. The dot means that this script should run in the context of the current shell. If this command -does not work in your shell, try replacing the dot with ``source``) +does not work in your shell, try replacing the dot with ``source``.) If you are a Windows user, the following command is for you:: @@ -95,15 +95,15 @@ your virtualenv:: A few seconds later you are good to go. -System Wide Installation +System-Wide Installation ------------------------ -This is possible as well, but I do not recommend it. Just run -`easy_install` with root rights:: +This is possible as well, though I do not recommend it. Just run +`easy_install` with root privileges:: $ sudo easy_install Flask -(Run it in an Admin shell on Windows systems and without `sudo`). +(Run it in an Admin shell on Windows systems and without `sudo`.) Living on the Edge @@ -159,7 +159,7 @@ to the `PATH` environment variable. To do that, right-click on the "Computer" icon on the Desktop or in the Start menu, and choose "Properties". Then, on Windows Vista and Windows 7 click on "Advanced System settings"; on Windows XP, click on the "Advanced" tab instead. Then click -on the "Environment variables" button and double click on the "Path" +on the "Environment variables" button and double-click on the "Path" variable in the "System variables" section. There append the path of your Python interpreter's Scripts folder; make sure you delimit it from existing values with a semicolon. Assuming you are using Python 2.6 on diff --git a/docs/quickstart.rst b/docs/quickstart.rst index df97d5ad..9b40dd13 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,7 +3,7 @@ Quickstart ========== -Eager to get started? This page gives a good introduction in how to get +Eager to get started? This page gives a good introduction on how to get started with Flask. This assumes you already have Flask installed. If you do not, head over to the :ref:`installation` section. @@ -39,16 +39,16 @@ So what did that code do? 1. First we imported the :class:`~flask.Flask` class. An instance of this class will be our WSGI application. The first argument is the name of - the application's module. If you are using a single module (like here) - you should use `__name__` because depending on if it's started as + the application's module. If you are using a single module (as in this example) + you should use `__name__` because depending on whether it's started as application or imported as module the name will be different (``'__main__'`` versus the actual import name). For more information - on that, have a look at the :class:`~flask.Flask` documentation. + on this technique, have a look at the :class:`~flask.Flask` documentation. 2. Next we create an instance of it. We pass it the name of the module / package. This is needed so that Flask knows where it should look for templates, static files and so on. 3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask - what URL should trigger our function. + which URL should trigger our function. 4. The function then has a name which is also used to generate URLs to that particular function, and returns the message we want to display in the user's browser. @@ -63,15 +63,14 @@ To stop the server, hit control-C. .. admonition:: Externally Visible Server - If you run the server you will notice that the server is only available + If you run the server you will notice that the server is only accessible from your own computer, not from any other in the network. This is the default because in debugging mode a user of the application can execute - arbitrary Python code on your computer. If you have `debug` disabled - or trust the users on your network, you can make the server publicly - available. + arbitrary Python code on your computer. - Just change the call of the :meth:`~flask.Flask.run` method to look - like this:: + If you have `debug` disabled or trust the users on your network, you can + make the server publicly available simply by changing the call of the + :meth:`~flask.Flask.run` method to look like this:: app.run(host='0.0.0.0') @@ -83,9 +82,9 @@ Debug Mode The :meth:`~flask.Flask.run` method is nice to start a local development server, but you would have to restart it manually after each -change you do to code. That is not very nice and Flask can do better. If -you enable the debug support the server will reload itself on code changes -and also provide you with a helpful debugger if things go wrong. +change to your code. That is not very nice and Flask can do better. If +you enable debug support the server will reload itself on code changes, +and it will also provide you with a helpful debugger if things go wrong. There are two ways to enable debugging. Either set that flag on the application object:: @@ -93,7 +92,7 @@ application object:: app.debug = True app.run() -Or pass it to run:: +Or pass it to `run`:: app.run(debug=True) @@ -123,7 +122,7 @@ Routing ------- Modern web applications have beautiful URLs. This helps people remember -the URLs which is especially handy for applications that are used from +the URLs, which is especially handy for applications that are used from mobile devices with slower network connections. If the user can directly go to the desired page without having to hit the index page it is more likely they will like the page and come back next time. @@ -171,8 +170,8 @@ The following converters exist: .. admonition:: Unique URLs / Redirection Behaviour Flask's URL rules are based on Werkzeug's routing module. The idea - behind that module is to ensure nice looking and also unique URLs based - on behaviour Apache and earlier servers coined. + behind that module is to ensure nice-looking and also unique URLs based + on behaviour coined by Apache and earlier servers. Take these two rules:: @@ -190,14 +189,14 @@ The following converters exist: that sense. Accessing it without a trailing slash will cause Flask to redirect to the canonical URL with the trailing slash. - However in the second case the URL is defined without a slash so it + However, in the second case the URL is defined without a slash so it behaves similar to a file and accessing the URL with a trailing slash - will be a 404 error. + will result in a 404 error. Why is this? This allows relative URLs to continue working if users access the page when they forget a trailing slash. This behaviour is also consistent with how Apache and other servers work. Also, the URLs - will stay unique which helps search engines not indexing the same page + will stay unique which ensures search engines do not index the same page twice. @@ -238,7 +237,7 @@ parameter. Here are some examples: (This also uses the :meth:`~flask.Flask.test_request_context` method explained below. It basically tells Flask to think we are handling a request even though we are not, we are in an interactive Python shell. -Have a look at the explanation below. :ref:`context-locals`). +Have a look at the explanation below: :ref:`context-locals`.) Why would you want to build URLs instead of hardcoding them in your templates? There are three good reasons for this: @@ -270,10 +269,10 @@ that can be changed by providing the `methods` argument to the If `GET` is present, `HEAD` will be added automatically for you. You don't have to deal with that. It will also make sure that `HEAD` requests -are handled like the `HTTP RFC`_ (the document describing the HTTP +are handled as the `HTTP RFC`_ (the document describing the HTTP protocol) demands, so you can completely ignore that part of the HTTP -specification. Likewise as of Flask 0.6, `OPTIONS` is implemented for you -as well automatically. +specification. Likewise, as of Flask 0.6, `OPTIONS` is implemented for you +automatically as well. You have no idea what an HTTP method is? Worry not, here is a quick introduction to HTTP methods and why they matter: @@ -297,14 +296,14 @@ very common: `POST` The browser tells the server that it wants to *post* some new information to that URL and that the server must ensure the data is - stored and only stored once. This is how HTML forms are usually - transmitting data to the server. + stored and only stored once. This is how HTML forms usually + transmit data to the server. `PUT` Similar to `POST` but the server might trigger the store procedure multiple times by overwriting the old values more than once. Now you - might be asking why is this useful, but there are some good reasons - to do it this way. Consider that the connection gets lost during + might be asking why this is useful, but there are some good reasons + to do it this way. Consider that the connection is lost during transmission: in this situation a system between the browser and the server might receive the request safely a second time without breaking things. With `POST` that would not be possible because it must only @@ -330,13 +329,13 @@ use it. Static Files ------------ -Dynamic web applications need static files as well. That's usually where +Dynamic web applications also need static files. That's usually where the CSS and JavaScript files are coming from. Ideally your web server is configured to serve them for you, but during development Flask can do that as well. Just create a folder called `static` in your package or next to your module and it will be available at `/static` on the application. -To generate URLs to that part of the URL, use the special ``'static'`` URL +To generate URLs that part of the URL, use the special ``'static'`` URL name:: url_for('static', filename='style.css') @@ -352,7 +351,7 @@ the application secure. Because of that Flask configures the `Jinja2 `_ template engine for you automatically. To render a template you can use the :func:`~flask.render_template` -method. All you have to do is to provide the name of the template and the +method. All you have to do is provide the name of the template and the variables you want to pass to the template engine as keyword arguments. Here's a simple example of how to render a template:: @@ -364,7 +363,7 @@ Here's a simple example of how to render a template:: return render_template('hello.html', name=name) Flask will look for templates in the `templates` folder. So if your -application is a module, that folder is next to that module, if it's a +application is a module, this folder is next to that module, if it's a package it's actually inside your package: **Case 1**: a module:: @@ -405,9 +404,9 @@ know how that works, head over to the :ref:`template-inheritance` pattern documentation. Basically template inheritance makes it possible to keep certain elements on each page (like header, navigation and footer). -Automatic escaping is enabled, so if name contains HTML it will be escaped +Automatic escaping is enabled, so if `name` contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be -safe HTML (because for example it came from a module that converts wiki +safe HTML (for example because it came from a module that converts wiki markup to HTML) you can mark it as safe by using the :class:`~jinja2.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. @@ -442,7 +441,7 @@ For web applications it's crucial to react to the data a client sent to the server. In Flask this information is provided by the global :class:`~flask.request` object. If you have some experience with Python you might be wondering how that object can be global and how Flask -manages to still be threadsafe. The answer are context locals: +manages to still be threadsafe. The answer is context locals: .. _context-locals: @@ -460,20 +459,20 @@ These objects are actually proxies to objects that are local to a specific context. What a mouthful. But that is actually quite easy to understand. Imagine the context being the handling thread. A request comes in and the -webserver decides to spawn a new thread (or something else, the -underlying object is capable of dealing with other concurrency systems -than threads as well). When Flask starts its internal request handling it +web server decides to spawn a new thread (or something else, the +underlying object is capable of dealing with concurrency systems other +than threads). When Flask starts its internal request handling it figures out that the current thread is the active context and binds the current application and the WSGI environments to that context (thread). -It does that in an intelligent way that one application can invoke another +It does that in an intelligent way so that one application can invoke another application without breaking. So what does this mean to you? Basically you can completely ignore that -this is the case unless you are doing something like unittesting. You -will notice that code that depends on a request object will suddenly break +this is the case unless you are doing something like unit testing. You +will notice that code which depends on a request object will suddenly break because there is no request object. The solution is creating a request object yourself and binding it to the context. The easiest solution for -unittesting is by using the :meth:`~flask.Flask.test_request_context` +unit testing is to use the :meth:`~flask.Flask.test_request_context` context manager. In combination with the `with` statement it will bind a test request so that you can interact with it. Here is an example:: @@ -680,8 +679,8 @@ converting return values into response objects is as follows: default parameters. 3. If a tuple is returned the response object is created by passing the tuple as arguments to the response object's constructor. -4. If neither of that works, Flask will assume the return value is a - valid WSGI application and converts that into a response object. +4. If none of that works, Flask will assume the return value is a + valid WSGI application and convert that into a response object. If you want to get hold of the resulting response object inside the view you can use the :func:`~flask.make_response` function. @@ -711,8 +710,8 @@ return it: Sessions -------- -Besides the request object there is also a second object called -:class:`~flask.session` that allows you to store information specific to a +In addition to the request object there is also a second object called +:class:`~flask.session` which allows you to store information specific to a user from one request to the next. This is implemented on top of cookies for you and signs the cookies cryptographically. What this means is that the user could look at the contents of your cookie but not modify it, @@ -752,12 +751,12 @@ sessions work:: # set the secret key. keep this really secret: app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RT' -The here mentioned :func:`~flask.escape` does escaping for you if you are -not using the template engine (like in this example). +The :func:`~flask.escape` mentioned here does escaping for you if you are +not using the template engine (as in this example). .. admonition:: How to generate good secret keys - The problem with random is that it's hard to judge what random is. And + The problem with random is that it's hard to judge what is truly random. And a secret key should be as random as possible. Your operating system has ways to generate pretty random stuff based on a cryptographic random generator which can be used to get such a key: @@ -775,9 +774,9 @@ Good applications and user interfaces are all about feedback. If the user does not get enough feedback they will probably end up hating the application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it -possible to record a message at the end of a request and access it next -request and only next request. This is usually combined with a layout -template that does this. +possible to record a message at the end of a request and access it on the next +(and only the next) request. This is usually combined with a layout +template to expose the message. To flash a message use the :func:`~flask.flash` method, to get hold of the messages you can use :func:`~flask.get_flashed_messages` which is also @@ -790,10 +789,10 @@ Logging .. versionadded:: 0.3 Sometimes you might be in a situation where you deal with data that -should be correct, but actually is not. For example you may have some client -side code that sends an HTTP request to the server but it's obviously -malformed. This might be caused by a user tempering with the data, or the -client code failing. Most of the time, it's okay to reply with ``400 Bad +should be correct, but actually is not. For example you may have some client-side +code that sends an HTTP request to the server but it's obviously +malformed. This might be caused by a user tampering with the data, or the +client code failing. Most of the time it's okay to reply with ``400 Bad Request`` in that situation, but sometimes that won't do and the code has to continue working. From 065afe53a6f72e42428c4373ed10d082868f25d6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 27 Dec 2011 19:08:44 +0100 Subject: [PATCH 0894/3747] Improved doc exaples. This fixes #370 and #371. --- docs/quickstart.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index df97d5ad..fb7de08c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -153,12 +153,12 @@ rule with ````. Here are some nice examples:: @app.route('/user/') def show_user_profile(username): # show the user profile for that user - pass + return 'User %s' % username @app.route('/post/') def show_post(post_id): # show the post with the given id, the id is an integer - pass + return 'Post %d' % post_id The following converters exist: @@ -178,11 +178,11 @@ The following converters exist: @app.route('/projects/') def projects(): - pass + return 'The project page' @app.route('/about') def about(): - pass + return 'The about page' They look rather similar, the difference is the trailing slash in the URL *definition*. In the first case, the canonical URL for the From b6625ec19329031ddb26dec63c6bd0477a00bf22 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 5 Jan 2012 00:04:50 +0100 Subject: [PATCH 0895/3747] Fixed a wrong example in the view docs. This fixes #374 --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index a48d81f2..092bb937 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -223,7 +223,7 @@ registration code:: app.add_url_rule(url, defaults={pk: None}, view_func=view_func, methods=['GET',]) app.add_url_rule(url, view_func=view_func, methods=['POST',]) - app.add_url_rule('%s<%s:%s>' % (url, pk, pk_type), view_func=view_func, + app.add_url_rule('%s<%s:%s>' % (url, pk_type, pk), view_func=view_func, methods=['GET', 'PUT', 'DELETE']) register_api(UserAPI, 'user_api', '/users/', pk='user_id') From d50a0b495566fa39ab30c79848dee3bdb3457201 Mon Sep 17 00:00:00 2001 From: awsum Date: Sat, 7 Jan 2012 21:11:35 +0200 Subject: [PATCH 0896/3747] typo fix --- docs/patterns/sqlalchemy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 5a33d1f6..270fa79e 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -157,7 +157,7 @@ Here is an example table and model (put this into `models.py`):: self.email = email def __repr__(self): - return '' % (self.name, self.email) + return '' % (self.name) users = Table('users', metadata, Column('id', Integer, primary_key=True), From 26a9c2079d77aba93dde6f95472f3d89d2d9783d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 7 Jan 2012 17:50:11 -0500 Subject: [PATCH 0897/3747] Add test to catch imports at Flask instantiation. --- flask/testsuite/helpers.py | 11 +++++++++++ flask/testsuite/test_apps/importerror.py | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 flask/testsuite/test_apps/importerror.py diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 41e31be9..ee365605 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -331,10 +331,21 @@ class LoggingTestCase(FlaskTestCase): '/myview/create') +class NoImportsTestCase(FlaskTestCase): + "Test Flasks are created without __import__." + + def test_name_with_import_error(self): + try: + flask.Flask('importerror') + except NotImplementedError: + self.fail('Flask(import_name) is importing import_name.') + + def suite(): suite = unittest.TestSuite() if flask.json_available: suite.addTest(unittest.makeSuite(JSONTestCase)) suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) + suite.addTest(unittest.makeSuite(NoImportsTestCase)) return suite diff --git a/flask/testsuite/test_apps/importerror.py b/flask/testsuite/test_apps/importerror.py new file mode 100644 index 00000000..eb298b9b --- /dev/null +++ b/flask/testsuite/test_apps/importerror.py @@ -0,0 +1,2 @@ +# NoImportsTestCase +raise NotImplementedError From d16491145dc553bc30aa9c71fb3647fda8d23534 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 6 Jan 2012 11:48:14 -0500 Subject: [PATCH 0898/3747] Update module helpers to avoid Python imports. This avoids errors in creating Flask instances where there are import errors in the module or package matching the import name. Those runtime errors will be apparent to the user soon enough, but tools which build Flask instances meta-programmatically benefit from a Flask which does not __import__. --- flask/helpers.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 7295dc3c..456f30cd 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -11,8 +11,10 @@ from __future__ import with_statement +import imp import os import sys +import pkgutil import posixpath import mimetypes from time import time @@ -492,14 +494,12 @@ def get_root_path(import_name): Not to be confused with the package path returned by :func:`find_package`. """ - __import__(import_name) - try: - directory = os.path.dirname(sys.modules[import_name].__file__) - return os.path.abspath(directory) - except AttributeError: - # this is necessary in case we are running from the interactive - # python shell. It will never be used for production code however + loader = pkgutil.get_loader(import_name) + if loader is None: return os.getcwd() + filepath = os.path.abspath(loader.get_filename(import_name)) + # filepath for import_name.py for a module, or __init__.py for a package. + return os.path.dirname(filepath) def find_package(import_name): @@ -510,16 +510,17 @@ def find_package(import_name): import the module. The prefix is the path below which a UNIX like folder structure exists (lib, share etc.). """ - __import__(import_name) - root_mod = sys.modules[import_name.split('.')[0]] - package_path = getattr(root_mod, '__file__', None) - if package_path is None: + root_mod_name = import_name.split('.')[0] + loader = pkgutil.get_loader(root_mod_name) + if loader is not None: + filename = loader.get_filename(root_mod_name) + package_path = os.path.abspath(os.path.dirname(filename)) + # package_path ends with __init__.py for a package + if loader.is_package(root_mod_name): + package_path = os.path.dirname(package_path) + else: # support for the interactive python shell package_path = os.getcwd() - else: - package_path = os.path.abspath(os.path.dirname(package_path)) - if hasattr(root_mod, '__path__'): - package_path = os.path.dirname(package_path) # leave the egg wrapper folder or the actual .egg on the filesystem test_package_path = package_path From a3b30b7e3b026e821716be63d586e078eaa6ee7f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 7 Jan 2012 17:54:37 -0500 Subject: [PATCH 0899/3747] Handle zip & interactive cases in module helpers. --- flask/helpers.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 456f30cd..48748dff 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -495,11 +495,18 @@ def get_root_path(import_name): Not to be confused with the package path returned by :func:`find_package`. """ loader = pkgutil.get_loader(import_name) - if loader is None: + if loader is None or import_name == '__main__': + # import name is not found, or interactive/main module return os.getcwd() - filepath = os.path.abspath(loader.get_filename(import_name)) - # filepath for import_name.py for a module, or __init__.py for a package. - return os.path.dirname(filepath) + # For .egg, zipimporter does not have get_filename until Python 2.7. + if hasattr(loader, 'get_filename'): + filepath = loader.get_filename(import_name) + else: + # Fall back to imports. + __import__(import_name) + filepath = sys.modules[import_name].__file__ + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) def find_package(import_name): @@ -512,24 +519,25 @@ def find_package(import_name): """ root_mod_name = import_name.split('.')[0] loader = pkgutil.get_loader(root_mod_name) - if loader is not None: - filename = loader.get_filename(root_mod_name) + if loader is None or import_name == '__main__': + # import name is not found, or interactive/main module + package_path = os.getcwd() + else: + # For .egg, zipimporter does not have get_filename until Python 2.7. + if hasattr(loader, 'get_filename'): + filename = loader.get_filename(root_mod_name) + else: + # zipimporter's loader.archive points to the .egg or .zip + # archive filename is dropped in call to dirname below. + filename = loader.archive package_path = os.path.abspath(os.path.dirname(filename)) # package_path ends with __init__.py for a package if loader.is_package(root_mod_name): package_path = os.path.dirname(package_path) - else: - # support for the interactive python shell - package_path = os.getcwd() - # leave the egg wrapper folder or the actual .egg on the filesystem - test_package_path = package_path - if os.path.basename(test_package_path).endswith('.egg'): - test_package_path = os.path.dirname(test_package_path) - - site_parent, site_folder = os.path.split(test_package_path) + site_parent, site_folder = os.path.split(package_path) py_prefix = os.path.abspath(sys.prefix) - if test_package_path.startswith(py_prefix): + if package_path.startswith(py_prefix): return py_prefix, package_path elif site_folder.lower() == 'site-packages': parent, folder = os.path.split(site_parent) From fde6e364a497cd44d049df17c93d1fd05ec09f90 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 7 Jan 2012 17:55:02 -0500 Subject: [PATCH 0900/3747] Update tests for new module helpers. --- flask/testsuite/config.py | 115 ++++++++++-------- .../lib/python2.5/site-packages/site_app.py | 3 + .../site-packages/site_package/__init__.py | 3 + flask/testsuite/test_apps/main_app.py | 4 + .../path/installed_package/__init__.py | 3 + 5 files changed, 77 insertions(+), 51 deletions(-) create mode 100644 flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py create mode 100644 flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py create mode 100644 flask/testsuite/test_apps/main_app.py create mode 100644 flask/testsuite/test_apps/path/installed_package/__init__.py diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index ad1721fd..e0f7005f 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -98,6 +98,14 @@ class InstanceTestCase(FlaskTestCase): app = flask.Flask(__name__, instance_path=here) self.assert_equal(app.instance_path, here) + def test_main_module_paths(self): + # Test an app with '__main__' as the import name, uses cwd. + from main_app import app + here = os.path.abspath(os.getcwd()) + self.assert_equal(app.instance_path, os.path.join(here, 'instance')) + if 'main_app' in sys.modules: + del sys.modules['main_app'] + def test_uninstalled_module_paths(self): from config_module_app import app here = os.path.abspath(os.path.dirname(__file__)) @@ -109,70 +117,75 @@ class InstanceTestCase(FlaskTestCase): self.assert_equal(app.instance_path, os.path.join(here, 'test_apps', 'instance')) def test_installed_module_paths(self): - import types - expected_prefix = os.path.abspath('foo') - mod = types.ModuleType('myapp') - mod.__file__ = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp.py') - sys.modules['myapp'] = mod + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + site_packages = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages') + sys.path.append(site_packages) try: - mod.app = flask.Flask(mod.__name__) - self.assert_equal(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) + import site_app + self.assert_equal(site_app.app.instance_path, + os.path.join(expected_prefix, 'var', + 'site_app-instance')) finally: - sys.modules['myapp'] = None + sys.prefix = real_prefix + sys.path.remove(site_packages) + if 'site_app' in sys.modules: + del sys.modules['site_app'] def test_installed_package_paths(self): - import types - expected_prefix = os.path.abspath('foo') - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + installed_path = os.path.join(expected_prefix, 'path') + sys.path.append(installed_path) try: - mod.app = flask.Flask(mod.__name__) - self.assert_equal(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) + import installed_package + self.assert_equal(installed_package.app.instance_path, + os.path.join(expected_prefix, 'var', + 'installed_package-instance')) finally: - sys.modules['myapp'] = None + sys.prefix = real_prefix + sys.path.remove(installed_path) + if 'installed_package' in sys.modules: + del sys.modules['installed_package'] - def test_prefix_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod + def test_prefix_package_paths(self): + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + site_packages = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages') + sys.path.append(site_packages) try: - mod.app = flask.Flask(mod.__name__) - self.assert_equal(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) + import site_package + self.assert_equal(site_package.app.instance_path, + os.path.join(expected_prefix, 'var', + 'site_package-instance')) finally: - sys.modules['myapp'] = None + sys.prefix = real_prefix + sys.path.remove(site_packages) + if 'site_package' in sys.modules: + del sys.modules['site_package'] def test_egg_installed_paths(self): - import types - expected_prefix = os.path.abspath(sys.prefix) - package_path = os.path.join(expected_prefix, 'lib', 'python2.5', - 'site-packages', 'MyApp.egg', 'myapp') - mod = types.ModuleType('myapp') - mod.__path__ = [package_path] - mod.__file__ = os.path.join(package_path, '__init__.py') - sys.modules['myapp'] = mod + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + site_packages = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages') + egg_path = os.path.join(site_packages, 'SiteEgg.egg') + sys.path.append(site_packages) + sys.path.append(egg_path) try: - mod.app = flask.Flask(mod.__name__) - self.assert_equal(mod.app.instance_path, - os.path.join(expected_prefix, 'var', - 'myapp-instance')) + import site_egg # in SiteEgg.egg + self.assert_equal(site_egg.app.instance_path, + os.path.join(expected_prefix, 'var', + 'site_egg-instance')) finally: - sys.modules['myapp'] = None + sys.prefix = real_prefix + sys.path.remove(site_packages) + sys.path.remove(egg_path) + if 'site_egg' in sys.modules: + del sys.modules['site_egg'] def suite(): diff --git a/flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py b/flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py new file mode 100644 index 00000000..06271108 --- /dev/null +++ b/flask/testsuite/test_apps/lib/python2.5/site-packages/site_app.py @@ -0,0 +1,3 @@ +import flask + +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py b/flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py new file mode 100644 index 00000000..06271108 --- /dev/null +++ b/flask/testsuite/test_apps/lib/python2.5/site-packages/site_package/__init__.py @@ -0,0 +1,3 @@ +import flask + +app = flask.Flask(__name__) diff --git a/flask/testsuite/test_apps/main_app.py b/flask/testsuite/test_apps/main_app.py new file mode 100644 index 00000000..a5f372c8 --- /dev/null +++ b/flask/testsuite/test_apps/main_app.py @@ -0,0 +1,4 @@ +import flask + +# Test Flask initialization with main module. +app = flask.Flask('__main__') diff --git a/flask/testsuite/test_apps/path/installed_package/__init__.py b/flask/testsuite/test_apps/path/installed_package/__init__.py new file mode 100644 index 00000000..06271108 --- /dev/null +++ b/flask/testsuite/test_apps/path/installed_package/__init__.py @@ -0,0 +1,3 @@ +import flask + +app = flask.Flask(__name__) From 970de5e8b67b6c5bbaac082e888f8ebc6e16cfb7 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 9 Jan 2012 10:25:06 -0500 Subject: [PATCH 0901/3747] Add discussion to NoImportsTestCase. --- flask/testsuite/helpers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index ee365605..89c6c188 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -332,7 +332,15 @@ class LoggingTestCase(FlaskTestCase): class NoImportsTestCase(FlaskTestCase): - "Test Flasks are created without __import__." + """Test Flasks are created without import. + + Avoiding ``__import__`` helps create Flask instances where there are errors + at import time. Those runtime errors will be apparent to the user soon + enough, but tools which build Flask instances meta-programmatically benefit + from a Flask which does not ``__import__``. Instead of importing to + retrieve file paths or metadata on a module or package, use the pkgutil and + imp modules in the Python standard library. + """ def test_name_with_import_error(self): try: From 83189f20bf9c3c026e7ecd9565b4373c4fea8d10 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 09:05:42 -0500 Subject: [PATCH 0902/3747] Add .egg for zipimporter instance path test. .gitignore contains '*.egg' --- .../lib/python2.5/site-packages/SiteEgg.egg | Bin 0 -> 1218 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 flask/testsuite/test_apps/lib/python2.5/site-packages/SiteEgg.egg diff --git a/flask/testsuite/test_apps/lib/python2.5/site-packages/SiteEgg.egg b/flask/testsuite/test_apps/lib/python2.5/site-packages/SiteEgg.egg new file mode 100644 index 0000000000000000000000000000000000000000..d80abe9309fb1a9d0c4fce1fd76e87679282a012 GIT binary patch literal 1218 zcmWIWW@Zs#U|`^2*i@|Uz;wUq#B3n%6cFX;jJGoR?{Kkt3Ux3^zEk*!FGo6E$(OtB+LU}ngynUfdIpE4;jX3_M? zGsB}6MkgkU<)*?p_uOI9m8szL6tXEP|qV1=vse3_t#S^V_I-b4$ zzW(eRn;Ub&6@*V*J$uIc^mYCZW!JxcDgJzX3gm$)b%*)?RnOF zUC;TGXHM%KTenJOzk;3mRvlAz6<#!VOlG@o#0<2b5r}1x-I0=7keZj0nwMM|pOcxF zT?}&2V48(%Sx&yo+-udjS>*?dE<9UVGTUYDcne&^246YbI_ymlm ztw9_>UzPg$KX;nQrFZtMugTxS3sC#m^5x3WYvV$zlMMB<}K6MufDxtZLf6euYpt#}$Vlia* zmE;%1=cJaU=77T)8obZZy@wn)DBi2eEYK}ZOiKj^2qTjSGw!qwv=t1NG=eDPlpo-Y z(1|U9LG&>&ENMK7suNiwwzP!MC=85NY$*%f1Z0nbQUMGsX`GF00=6`PZWMZwKp1rs z*(g{7LD!BRV+ie)Oh|q~i9d8RkbMJ+HW*maxC7Y?SR@8`v$BDdumj;Cpc`KS6)`XX E0770`LI3~& literal 0 HcmV?d00001 From 1f20a11284e3e9bcfc08939c48e3ab590da7c7a4 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 09:23:40 -0500 Subject: [PATCH 0903/3747] Fall back to imports w/exotic pkg loaders, #380. Needs a test, which likely requires introducing a mock library. --- flask/helpers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 48748dff..3c9a5669 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -526,10 +526,17 @@ def find_package(import_name): # For .egg, zipimporter does not have get_filename until Python 2.7. if hasattr(loader, 'get_filename'): filename = loader.get_filename(root_mod_name) - else: + elif hasattr(loader, 'archive'): # zipimporter's loader.archive points to the .egg or .zip # archive filename is dropped in call to dirname below. filename = loader.archive + else: + # At least one loader is missing both get_filename and archive: + # Google App Engine's HardenedModulesHook + # + # Fall back to imports. + __import__(import_name) + filename = sys.modules[import_name].__file__ package_path = os.path.abspath(os.path.dirname(filename)) # package_path ends with __init__.py for a package if loader.is_package(root_mod_name): From a59cbd4f5256a356fcb7858bf0a5a0ad535e8e6d Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Thu, 8 Dec 2011 14:41:29 +0200 Subject: [PATCH 0904/3747] Minor grammar fix --- flask/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index f11c3ddd..3e35e46f 100644 --- a/flask/views.py +++ b/flask/views.py @@ -146,5 +146,5 @@ class MethodView(View): # retry with GET if meth is None and request.method == 'HEAD': meth = getattr(self, 'get', None) - assert meth is not None, 'Not implemented method %r' % request.method + assert meth is not None, 'Unimplemented method %r' % request.method return meth(*args, **kwargs) From d18868bd17f612d5598b8e8cc29800ae84c37620 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Thu, 8 Dec 2011 17:36:52 +0200 Subject: [PATCH 0905/3747] Add simple decorator example Github issue #358 --- docs/views.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/views.rst b/docs/views.rst index 092bb937..feee0a8b 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -144,14 +144,22 @@ routing system it does not make much sense to decorate the class itself. Instead you either have to decorate the return value of :meth:`~flask.views.View.as_view` by hand:: - view = rate_limited(UserAPI.as_view('users')) + def user_required(f): + """Checks whether user is logged in or raises error 401.""" + def decorator(*args, **kwargs): + if not g.user: + abort(401) + return f(*args, **kwargs) + return decorator + + view = user_required(UserAPI.as_view('users')) app.add_url_rule('/users/', view_func=view) Starting with Flask 0.8 there is also an alternative way where you can specify a list of decorators to apply in the class declaration:: class UserAPI(MethodView): - decorators = [rate_limited] + decorators = [user_required] Due to the implicit self from the caller's perspective you cannot use regular view decorators on the individual methods of the view however, From e2cb8d2ef195df8ffc76587d136182b431b96983 Mon Sep 17 00:00:00 2001 From: ThomasWaldmann Date: Fri, 30 Sep 2011 14:03:01 +0300 Subject: [PATCH 0906/3747] fixed typo, quote "with" statement --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 61723dca..2fed4b4a 100644 --- a/CHANGES +++ b/CHANGES @@ -70,7 +70,7 @@ Released on September 29th 2011, codename Rakija as defaults. - Added :attr:`flask.views.View.decorators` to support simpler decorating of pluggable (class based) views. -- Fixed an issue where the test client if used with the with statement did not +- Fixed an issue where the test client if used with the "with" statement did not trigger the execution of the teardown handlers. - Added finer control over the session cookie parameters. - HEAD requests to a method view now automatically dispatch to the `get` From 94692607e6247a919f952375d922e930751cadcf Mon Sep 17 00:00:00 2001 From: Sundar Raman Date: Wed, 2 Nov 2011 15:18:04 -0400 Subject: [PATCH 0907/3747] Documentation on how to use external debuggers like eclipse/aptana --- docs/quickstart.rst | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9bf331ae..be3cecdd 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -114,8 +114,40 @@ Screenshot of the debugger in action: .. admonition:: Working With Other Debuggers - Debuggers interfere with each other. If you are using another debugger - (e.g. PyDev or IntelliJ), you may need to set ``app.debug = False``. + Debuggers interfere with each other. + That said, you may still wish to use the debugger in a tool of your choice. + Flask provides the following options to manage the debug process: + + * ``debug`` - whether to enable debug mode and catch exceptinos + * ``use_debugger`` - whether to use the internal Flask debugger + * ``use_reloader`` - whether to reload and fork the process on exception + + ``debug`` must be True (i.e., exceptions must caught) in order for the + other two options to have any value. + + If you're using Aptana/Eclipse for debugging you'll need to set both + ``use_debugger`` and ``use_reloader`` to False. + + A possible useful pattern for configuration is to set the following in your + config.yaml (change the block as approriate for your application, of course):: + + FLASK: + DEBUG: True + DEBUG_WITH_APTANA: True + + Then in your application's entry-point (main.py), you could have something like:: + + if __name__ == "__main__": + # To allow aptana to receive errors, set use_debugger=False + app = create_app(config="config.yaml") + + if app.debug: use_debugger = True + try: + # Disable Flask's debugger if external debugger is requested + use_debugger = not(app.config.get('DEBUG_WITH_APTANA')) + except: + pass + app.run(use_debugger=use_debugger, debug=app.debug, use_reloader=use_debugger, host='0.0.0.0') Routing From d620ea7ea2be35b4757f27fdca49be91d8dfd165 Mon Sep 17 00:00:00 2001 From: awsum Date: Fri, 13 Jan 2012 01:36:15 +0200 Subject: [PATCH 0908/3747] Update docs/api.rst a bit to reflect code. --- docs/api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 47ff84e5..d2b62199 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -146,6 +146,10 @@ Response Objects .. attribute:: status + A string with a response status. + + .. attribute:: status_code + The response status as integer. From 46651659c29a3ff8fa470e32d3e35f9cc2c7fbc0 Mon Sep 17 00:00:00 2001 From: Kyle Wild Date: Sun, 23 Oct 2011 16:41:00 -0700 Subject: [PATCH 0909/3747] Fix a typo ("is"->"if") in the comments; clarify a bit --- flask/ctx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flask/ctx.py b/flask/ctx.py index 9a72d251..0c2082e3 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -21,9 +21,9 @@ class _RequestGlobals(object): def has_request_context(): """If you have code that wants to test if a request context is there or - not this function can be used. For instance if you want to take advantage - of request information is it's available but fail silently if the request - object is unavailable. + not this function can be used. For instance, if you want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. :: From 8532bd51a709263907a4604660b8dabf51e39ab2 Mon Sep 17 00:00:00 2001 From: Kyle Wild Date: Sun, 23 Oct 2011 16:41:59 -0700 Subject: [PATCH 0910/3747] [docstring] Remove an extra `if` clause to clarify sentence --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 0c2082e3..47ac0cc1 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -21,7 +21,7 @@ class _RequestGlobals(object): def has_request_context(): """If you have code that wants to test if a request context is there or - not this function can be used. For instance, if you want to take advantage + not this function can be used. For instance, you may want to take advantage of request information if the request object is available, but fail silently if it is unavailable. From 2a4d3ef1168e0ea1733898527229961c9ddafced Mon Sep 17 00:00:00 2001 From: Reisen Date: Fri, 8 Jul 2011 15:46:16 +0100 Subject: [PATCH 0911/3747] Added a template filter decorator to blueprints. --- flask/blueprints.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flask/blueprints.py b/flask/blueprints.py index ccdda38d..54fa33bc 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -185,6 +185,17 @@ class Blueprint(_PackageBoundObject): return f return decorator + def app_template_filter(self, name = None): + """Like :meth:`Flask.template_filter` but for a blueprint. The filter + is available for the entire application. + """ + def decorator(f): + def register_template(state): + state.app.jinja_env.filters[name or f.__name__] = f + self.record_once(register_template) + return f + return decorator + def before_request(self, f): """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of From ce4d589d5b7bbda46b9c34211dcd3e1c90ee83c9 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 20:16:48 -0500 Subject: [PATCH 0912/3747] Add non-decorator template filter methods. Suggested by @Poincare on GitHub, on @Reisen's pull request: https://github.com/mitsuhiko/flask/pull/272 --- flask/app.py | 12 +++++++++++- flask/blueprints.py | 25 +++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/flask/app.py b/flask/app.py index 42ffea4d..0e462020 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1018,10 +1018,20 @@ class Flask(_PackageBoundObject): function name will be used. """ def decorator(f): - self.jinja_env.filters[name or f.__name__] = f + self.add_template_filter(f, name=name) return f return decorator + @setupmethod + def add_template_filter(self, f, name=None): + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" diff --git a/flask/blueprints.py b/flask/blueprints.py index 54fa33bc..d81d3c73 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -185,17 +185,30 @@ class Blueprint(_PackageBoundObject): return f return decorator - def app_template_filter(self, name = None): - """Like :meth:`Flask.template_filter` but for a blueprint. The filter - is available for the entire application. + def app_template_filter(self, name=None): + """Register a custom template filter, available application wide. Like + :meth:`Flask.template_filter` but for a blueprint. + + :param name: the optional name of the filter, otherwise the + function name will be used. """ def decorator(f): - def register_template(state): - state.app.jinja_env.filters[name or f.__name__] = f - self.record_once(register_template) + self.add_app_template_filter(f, name=name) return f return decorator + def add_app_template_filter(self, f, name=None): + """Register a custom template filter, available application wide. Like + :meth:`Flask.add_template_filter` but for a blueprint. Works exactly + like the :meth:`app_template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + def register_template(state): + state.app.jinja_env.filters[name or f.__name__] = f + self.record_once(register_template) + def before_request(self, f): """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of From 820d099e82aa8ee01e1e82b85e2c6636d63be98a Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 20:21:26 -0500 Subject: [PATCH 0913/3747] Add tests for template filter methods/decorators. --- flask/testsuite/blueprints.py | 109 ++++++++++++++++++++++++++++++++++ flask/testsuite/templating.py | 40 +++++++++++++ 2 files changed, 149 insertions(+) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 3f65dd48..5bf81d92 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -504,6 +504,115 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/py/bar/123') assert rv.status_code == 404 + def test_template_filter(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() + def my_reverse(s): + return s[::-1] + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + + def test_add_template_filter(self): + bp = flask.Blueprint('bp', __name__) + def my_reverse(s): + return s[::-1] + bp.add_app_template_filter(my_reverse) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + + def test_template_filter_with_name(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter('strrev') + def my_reverse(s): + return s[::-1] + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + + def test_add_template_filter_with_name(self): + bp = flask.Blueprint('bp', __name__) + def my_reverse(s): + return s[::-1] + bp.add_app_template_filter(my_reverse, 'strrev') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + + def test_template_filter_with_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() + def super_reverse(s): + return s[::-1] + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_template_filter_after_route_with_template(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() + def super_reverse(s): + return s[::-1] + app.register_blueprint(bp, url_prefix='/py') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_add_template_filter_with_template(self): + bp = flask.Blueprint('bp', __name__) + def super_reverse(s): + return s[::-1] + bp.add_app_template_filter(super_reverse) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_template_filter_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter('super_reverse') + def my_reverse(s): + return s[::-1] + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + + def test_add_template_filter_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + def my_reverse(s): + return s[::-1] + bp.add_app_template_filter(my_reverse, 'super_reverse') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 453bfb65..759fe0f3 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -93,6 +93,15 @@ class TemplatingTestCase(FlaskTestCase): self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + def test_add_template_filter(self): + app = flask.Flask(__name__) + def my_reverse(s): + return s[::-1] + app.add_template_filter(my_reverse) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) + self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') + def test_template_filter_with_name(self): app = flask.Flask(__name__) @app.template_filter('strrev') @@ -102,6 +111,15 @@ class TemplatingTestCase(FlaskTestCase): self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + def test_add_template_filter_with_name(self): + app = flask.Flask(__name__) + def my_reverse(s): + return s[::-1] + app.add_template_filter(my_reverse, 'strrev') + self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) + self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') + def test_template_filter_with_template(self): app = flask.Flask(__name__) @app.template_filter() @@ -113,6 +131,17 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_add_template_filter_with_template(self): + app = flask.Flask(__name__) + def super_reverse(s): + return s[::-1] + app.add_template_filter(super_reverse) + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + def test_template_filter_with_name_and_template(self): app = flask.Flask(__name__) @app.template_filter('super_reverse') @@ -124,6 +153,17 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_add_template_filter_with_name_and_template(self): + app = flask.Flask(__name__) + def my_reverse(s): + return s[::-1] + app.add_template_filter(my_reverse, 'super_reverse') + @app.route('/') + def index(): + return flask.render_template('template_filter.html', value='abcd') + rv = app.test_client().get('/') + self.assert_equal(rv.data, 'dcba') + def test_custom_template_loader(self): class MyFlask(flask.Flask): def create_global_jinja_loader(self): From 96f7beba46c52cfa4d9231d066271564f995e3c0 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 20:53:20 -0500 Subject: [PATCH 0914/3747] Document recent changes. --- CHANGES | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 2fed4b4a..6f360814 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,18 @@ Relase date to be decided, codename to be chosen. - Session is now stored after callbacks so that if the session payload is stored in the session you can still modify it in an after request callback. +- The :class:`flask.Flask` class will avoid importing the provided import name + if it can (the required first parameter), to benefit tools which build Flask + instances programmatically. The Flask class will fall back to using import + on systems with custom module hooks, e.g. Google App Engine, or when the + import name is inside a zip archive (usually a .egg) prior to Python 2.7. +- Blueprints now have a decorator to add custom template filters application + wide, :meth:`flask.Blueprint.app_template_filter`. +- The Flask and Blueprint classes now have a non-decorator method for adding + custom template filters application wide, + :meth:`flask.Flask.add_template_filter` and + :meth:`flask.Blueprint.add_app_template_filter`. + Version 0.8.1 ------------- From e5161a773e2d562aed2df96a0c80b34004022cef Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Sat, 25 Jun 2011 18:34:33 -0700 Subject: [PATCH 0915/3747] commas after introductory phrases --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index c4ded1fe..ef57e07c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Welcome to Flask Welcome to Flask's documentation. This documentation is divided into different parts. I recommend that you get started with :ref:`installation` and then head over to the :ref:`quickstart`. -Besides the quickstart there is also a more detailed :ref:`tutorial` that +Besides the quickstart, there is also a more detailed :ref:`tutorial` that shows how to create a complete (albeit small) application with Flask. If you'd rather dive into the internals of Flask, check out the :ref:`api` documentation. Common patterns are described in the @@ -18,7 +18,7 @@ the :ref:`api` documentation. Common patterns are described in the Flask depends on two external libraries: the `Jinja2`_ template engine and the `Werkzeug`_ WSGI toolkit. These libraries are not documented -here. If you want to dive into their documentation check out the +here. If you want to dive into their documentation, check out the following links: - `Jinja2 Documentation `_ From 1a9e082a76df4b7433c0dda5d22e05b1e7c2c263 Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Sat, 25 Jun 2011 19:14:34 -0700 Subject: [PATCH 0916/3747] tweaks to style --- docs/installation.rst | 110 +++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 55065b6d..d63a5842 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -9,38 +9,37 @@ Werkzeug is a toolkit for WSGI, the standard Python interface between web applications and a variety of servers for both development and deployment. Jinja2 renders templates. -So how do you get all that on your computer quickly? There are many ways, -as this section will explain, but the most kick-ass method is -virtualenv, so let's look at that first. +So how do you get all that on your computer quickly? There are many ways you +could do that, but the most kick-ass method is virtualenv, so let's have a look +at that first. -Either way, you will need Python 2.5 or higher to get started, so be sure -to have an up to date Python 2.x installation. At the time of writing, -the WSGI specification is not yet finalized for Python 3, so Flask cannot -support the 3.x series of Python. +You will need Python 2.5 or higher to get started, so be sure to have an +up-to-date Python 2.x installation. At the time of writing, the WSGI +specification has not yet been finalized for Python 3, so Flask cannot support +the 3.x series of Python. .. _virtualenv: virtualenv ---------- -Virtualenv is probably what you want to use during development, and in -production too if you have shell access there. +Virtualenv is probably what you want to use during development, and if you have +shell access to your production machines, you'll probably want to use it there, +too. -What problem does virtualenv solve? If you like Python as I do, -chances are you want to use it for other projects besides Flask-based -web applications. But the more projects you have, the more likely it is -that you will be working with different versions of Python itself, or at -least different versions of Python libraries. Let's face it; quite often -libraries break backwards compatibility, and it's unlikely that any serious -application will have zero dependencies. So what do you do if two or more -of your projects have conflicting dependencies? +What problem does virtualenv solve? If you like Python as much as I do, +chances are you want to use it for other projects besides Flask-based web +applications. But the more projects you have, the more likely it is that you +will be working with different versions of Python itself, or at least different +versions of Python libraries. Let's face it: quite often libraries break +backwards compatibility, and it's unlikely that any serious application will +have zero dependencies. So what do you do if two or more of your projects have +conflicting dependencies? -Virtualenv to the rescue! It basically enables multiple side-by-side -installations of Python, one for each project. It doesn't actually -install separate copies of Python, but it does provide a clever way -to keep different project environments isolated. - -So let's see how virtualenv works! +Virtualenv to the rescue! Virtualenv enables multiple side-by-side +installations of Python, one for each project. It doesn't actually install +separate copies of Python, but it does provide a clever way to keep different +project environments isolated. Let's see how virtualenv works. If you are on Mac OS X or Linux, chances are that one of the following two commands will work for you:: @@ -51,15 +50,15 @@ or even better:: $ sudo pip install virtualenv -One of these will probably install virtualenv on your system. Maybe it's -even in your package manager. If you use Ubuntu, try:: +One of these will probably install virtualenv on your system. Maybe it's even +in your package manager. If you use Ubuntu, try:: $ sudo apt-get install python-virtualenv If you are on Windows and don't have the `easy_install` command, you must install it first. Check the :ref:`windows-easy-install` section for more -information on how to do that. Once you have it installed, run the -same commands as above, but without the `sudo` prefix. +information about how to do that. Once you have it installed, run the same +commands as above, but without the `sudo` prefix. Once you have virtualenv installed, just fire up a shell and create your own environment. I usually create a project folder and an `env` @@ -71,28 +70,28 @@ folder within:: New python executable in env/bin/python Installing setuptools............done. -Now, whenever you want to work on a project, you only have to activate -the corresponding environment. On OS X and Linux, do the following:: +Now, whenever you want to work on a project, you only have to activate the +corresponding environment. On OS X and Linux, do the following:: $ . env/bin/activate -(Note the space between the dot and the script name. The dot means that -this script should run in the context of the current shell. If this command -does not work in your shell, try replacing the dot with ``source``.) +(Note the space between the dot and the script name. The dot means that this +script should run in the context of the current shell. If this command does +not work in your shell, try replacing the dot with ``source``) If you are a Windows user, the following command is for you:: $ env\scripts\activate -Either way, you should now be using your virtualenv (see how the prompt of +Either way, you should now be using your virtualenv (notice how the prompt of your shell has changed to show the virtualenv). -Now you can just enter the following command to get Flask activated in -your virtualenv:: +Now you can just enter the following command to get Flask activated in your +virtualenv:: $ easy_install Flask -A few seconds later you are good to go. +A few seconds later and you are good to go. System-Wide Installation @@ -103,15 +102,16 @@ This is possible as well, though I do not recommend it. Just run $ sudo easy_install Flask -(Run it in an Admin shell on Windows systems and without `sudo`.) +(On Windows systems, run it in a command-prompt window with administrator +privleges, and leave out `sudo`.) Living on the Edge ------------------ If you want to work with the latest version of Flask, there are two ways: you -can either let `easy_install` pull in the development version, or tell it -to operate on a git checkout. Either way, virtualenv is recommended. +can either let `easy_install` pull in the development version, or you can tell +it to operate on a git checkout. Either way, virtualenv is recommended. Get the git checkout in a new virtualenv and run in development mode:: @@ -127,8 +127,8 @@ Get the git checkout in a new virtualenv and run in development mode:: Finished processing dependencies for Flask This will pull in the dependencies and activate the git head as the current -version inside the virtualenv. Then you just have to ``git pull origin`` -to get the latest version. +version inside the virtualenv. Then all you have to do is run ``git pull +origin`` to update to the latest version. To just get the development version without git, do this instead:: @@ -147,29 +147,27 @@ To just get the development version without git, do this instead:: `easy_install` on Windows ------------------------- -On Windows, installation of `easy_install` is a little bit trickier because -slightly different rules apply on Windows than on Unix-like systems, but -it's not difficult. The easiest way to do it is to download the -`ez_setup.py`_ file and run it. The easiest way to run the file is to -open your downloads folder and double-click on the file. +On Windows, installation of `easy_install` is a little bit trickier, but still +quite easy. The easiest way to do it is to download the `ez_setup.py`_ file +and run it. The easiest way to run the file is to open your downloads folder +and double-click on the file. Next, add the `easy_install` command and other Python scripts to the command search path, by adding your Python installation's Scripts folder to the `PATH` environment variable. To do that, right-click on the -"Computer" icon on the Desktop or in the Start menu, and choose -"Properties". Then, on Windows Vista and Windows 7 click on "Advanced System -settings"; on Windows XP, click on the "Advanced" tab instead. Then click -on the "Environment variables" button and double-click on the "Path" -variable in the "System variables" section. There append the path of your -Python interpreter's Scripts folder; make sure you delimit it from -existing values with a semicolon. Assuming you are using Python 2.6 on +"Computer" icon on the Desktop or in the Start menu, and choose "Properties". +Then click on "Advanced System settings" (on Windows XP, click on the +"Advanced" tab instead). Then click on the "Environment variables" button and +double-click on the "Path" variable in the "System variables" section. There +append the path of your Python interpreter's Scripts folder. Be sure to delimit +it from existing values with a semicolon. Assuming you are using Python 2.6 on the default path, add the following value:: ;C:\Python26\Scripts -Then you are done. To check that it worked, open the Command Prompt and -execute ``easy_install``. If you have User Account Control enabled on -Windows Vista or Windows 7, it should prompt you for admin privileges. +And you are done! To check that it worked, open the Command Prompt and execute +``easy_install``. If you have User Account Control enabled on Windows Vista or +Windows 7, it should prompt you for administrator privileges. .. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py From c34a87033f19174029083fe928c9a65acbeb24d5 Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Sat, 25 Jun 2011 19:16:44 -0700 Subject: [PATCH 0917/3747] tweaks to style and structure --- docs/installation.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index d63a5842..a7add061 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -156,12 +156,13 @@ Next, add the `easy_install` command and other Python scripts to the command search path, by adding your Python installation's Scripts folder to the `PATH` environment variable. To do that, right-click on the "Computer" icon on the Desktop or in the Start menu, and choose "Properties". -Then click on "Advanced System settings" (on Windows XP, click on the -"Advanced" tab instead). Then click on the "Environment variables" button and -double-click on the "Path" variable in the "System variables" section. There -append the path of your Python interpreter's Scripts folder. Be sure to delimit -it from existing values with a semicolon. Assuming you are using Python 2.6 on -the default path, add the following value:: +Then click on "Advanced System settings" (in Windows XP, click on the +"Advanced" tab instead). Then click on the "Environment variables" button. +Finally, double-click on the "Path" variable in the "System variables" section, +and add the path of your Python interpreter's Scripts folder. Be sure to +delimit it from existing values with a semicolon. Assuming you are using +Python 2.6 on the default path, add the following value:: + ;C:\Python26\Scripts From 593b003b5ca40e4a4a7d2a4319afaad91eac5c62 Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Sat, 25 Jun 2011 19:17:37 -0700 Subject: [PATCH 0918/3747] tweak to one sentence --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index a7add061..f02b5cb1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -84,7 +84,7 @@ If you are a Windows user, the following command is for you:: $ env\scripts\activate Either way, you should now be using your virtualenv (notice how the prompt of -your shell has changed to show the virtualenv). +your shell has changed to show the active environment). Now you can just enter the following command to get Flask activated in your virtualenv:: From df19ee95bda35825521ddaf210cbe17659d8f6bf Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Sat, 25 Jun 2011 19:55:01 -0700 Subject: [PATCH 0919/3747] more editorial work --- docs/quickstart.rst | 155 ++++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index be3cecdd..2404361d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,9 +3,9 @@ Quickstart ========== -Eager to get started? This page gives a good introduction on how to get -started with Flask. This assumes you already have Flask installed. If -you do not, head over to the :ref:`installation` section. +Eager to get started? This page gives a good introduction to Flask. It +assumes you already have Flask installed. If you do not, head over to the +:ref:`installation` section. A Minimal Application @@ -23,39 +23,39 @@ A minimal Flask application looks something like this:: if __name__ == '__main__': app.run() -Just save it as `hello.py` or something similar and run it with your -Python interpreter. Make sure to not call your application `flask.py` -because this would conflict with Flask itself. +Just save it as `hello.py` (or something similar) and run it with your Python +interpreter. Make sure to not call your application `flask.py` because this +would conflict with Flask itself. :: $ python hello.py * Running on http://127.0.0.1:5000/ -Head over to `http://127.0.0.1:5000/ `_, you should -see your hello world greeting. +Now head over to `http://127.0.0.1:5000/ `_, and you +should see your hello world greeting. So what did that code do? 1. First we imported the :class:`~flask.Flask` class. An instance of this class will be our WSGI application. The first argument is the name of - the application's module. If you are using a single module (as in this example) - you should use `__name__` because depending on whether it's started as - application or imported as module the name will be different - (``'__main__'`` versus the actual import name). For more information - on this technique, have a look at the :class:`~flask.Flask` documentation. -2. Next we create an instance of it. We pass it the name of the module / - package. This is needed so that Flask knows where it should look for - templates, static files and so on. -3. Then we use the :meth:`~flask.Flask.route` decorator to tell Flask - which URL should trigger our function. -4. The function then has a name which is also used to generate URLs to - that particular function, and returns the message we want to display in - the user's browser. -5. Finally we use the :meth:`~flask.Flask.run` function to run the - local server with our application. The ``if __name__ == '__main__':`` - makes sure the server only runs if the script is executed directly from - the Python interpreter and not used as imported module. + the application's module. If you are using a single module (as in this + example), you should use `__name__` because depending on if it's started as + application or imported as module the name will be different (``'__main__'`` + versus the actual import name). For more information, have a look at the + :class:`~flask.Flask` documentation. +2. Next we create an instance of this class. We pass it the name of the module + or package. This is needed so that Flask knows where to look for templates, + static files, and so on. +3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask what URL + should trigger our function. +4. The function is given a name which is also used to generate URLs for that + particular function, and returns the message we want to display in the + user's browser. +5. Finally we use the :meth:`~flask.Flask.run` function to run the local server + with our application. The ``if __name__ == '__main__':`` makes sure the + server only runs if the script is executed directly from the Python + interpreter and not used as imported module. To stop the server, hit control-C. @@ -74,7 +74,7 @@ To stop the server, hit control-C. app.run(host='0.0.0.0') - This tells your operating system to listen on a public IP. + This tells your operating system to listen on all public IPs. Debug Mode @@ -92,18 +92,18 @@ application object:: app.debug = True app.run() -Or pass it to `run`:: +Or pass it as a parameter to run:: app.run(debug=True) -Both will have exactly the same effect. +Both methods have the exact same effect. .. admonition:: Attention Even though the interactive debugger does not work in forking environments (which makes it nearly impossible to use on production servers), it still - allows the execution of arbitrary code. That makes it a major security - risk and therefore it **must never be used on production machines**. + allows the execution of arbitrary code. This makes it a major security risk + and therefore it **must never be used on production machines**. Screenshot of the debugger in action: @@ -114,21 +114,21 @@ Screenshot of the debugger in action: .. admonition:: Working With Other Debuggers - Debuggers interfere with each other. - That said, you may still wish to use the debugger in a tool of your choice. + Debuggers interfere with each other. + That said, you may still wish to use the debugger in a tool of your choice. Flask provides the following options to manage the debug process: * ``debug`` - whether to enable debug mode and catch exceptinos * ``use_debugger`` - whether to use the internal Flask debugger * ``use_reloader`` - whether to reload and fork the process on exception - ``debug`` must be True (i.e., exceptions must caught) in order for the + ``debug`` must be True (i.e., exceptions must caught) in order for the other two options to have any value. - If you're using Aptana/Eclipse for debugging you'll need to set both + If you're using Aptana/Eclipse for debugging you'll need to set both ``use_debugger`` and ``use_reloader`` to False. - A possible useful pattern for configuration is to set the following in your + A possible useful pattern for configuration is to set the following in your config.yaml (change the block as approriate for your application, of course):: FLASK: @@ -159,8 +159,8 @@ mobile devices with slower network connections. If the user can directly go to the desired page without having to hit the index page it is more likely they will like the page and come back next time. -As you have seen above, the :meth:`~flask.Flask.route` decorator is used -to bind a function to a URL. Here are some basic examples:: +As you have seen above, the :meth:`~flask.Flask.route` decorator is used to +bind a function to a URL. Here are some basic examples:: @app.route('/') def index(): @@ -170,16 +170,16 @@ to bind a function to a URL. Here are some basic examples:: def hello(): return 'Hello World' -But there is more to it! You can make certain parts of the URL dynamic -and attach multiple rules to a function. +But there is more to it! You can make certain parts of the URL dynamic and +attach multiple rules to a function. Variable Rules `````````````` To add variable parts to a URL you can mark these special sections as -````. Such a part is then passed as keyword argument to -your function. Optionally a converter can be specified by specifying a -rule with ````. Here are some nice examples:: +````. Such a part is then passed as keyword argument to your +function. Optionally a converter can be specified by specifying a rule with +````. Here are some nice examples:: @app.route('/user/') def show_user_profile(username): @@ -201,9 +201,9 @@ The following converters exist: .. admonition:: Unique URLs / Redirection Behaviour - Flask's URL rules are based on Werkzeug's routing module. The idea - behind that module is to ensure nice-looking and also unique URLs based - on behaviour coined by Apache and earlier servers. + Flask's URL rules are based on Werkzeug's routing module. The idea behind + that module is to ensure beautiful and unique also unique URLs based on + precedents laid down by Apache and earlier HTTP servers. Take these two rules:: @@ -215,21 +215,20 @@ The following converters exist: def about(): return 'The about page' - They look rather similar, the difference is the trailing slash in the - URL *definition*. In the first case, the canonical URL for the - `projects` endpoint has a trailing slash. It's similar to a folder in - that sense. Accessing it without a trailing slash will cause Flask to - redirect to the canonical URL with the trailing slash. + Though they look rather similar, they differ in their use of the trailing + slash in the URL *definition*. In the first case, the canonical URL for the + `projects` endpoint has a trailing slash. In that sense, it is similar to + a folder on a file system. Accessing it without a trailing slash will cause + Flask to redirect to the canonical URL with the trailing slash. - However, in the second case the URL is defined without a slash so it - behaves similar to a file and accessing the URL with a trailing slash - will result in a 404 error. + In the second case, however, the URL is defined without a trailing slash, + rather like the pathname of a file on UNIX-like systems. Accessing the URL + with a trailing slash will produce a 404 "Not Found" error. - Why is this? This allows relative URLs to continue working if users - access the page when they forget a trailing slash. This behaviour is - also consistent with how Apache and other servers work. Also, the URLs - will stay unique which ensures search engines do not index the same page - twice. + This behavior allows relative URLs to continue working if users access the + page when they forget a trailing slash, consistent with how with how Apache + and other servers work. Also, the URLs will stay unique, which helps search + engines avoid indexing the same page twice. .. _url-building: @@ -237,12 +236,12 @@ The following converters exist: URL Building ```````````` -If it can match URLs, can it also generate them? Of course it can. To +If it can match URLs, can Flask also generate them? Of course it can. To build a URL to a specific function you can use the :func:`~flask.url_for` -function. It accepts the name of the function as first argument and a -number of keyword arguments, each corresponding to the variable part of -the URL rule. Unknown variable parts are appended to the URL as query -parameter. Here are some examples: +function. It accepts the name of the function as first argument and a number +of keyword arguments, each corresponding to the variable part of the URL rule. +Unknown variable parts are appended to the URL as query parameters. Here are +some examples: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -266,30 +265,30 @@ parameter. Here are some examples: /login?next=/ /user/John%20Doe -(This also uses the :meth:`~flask.Flask.test_request_context` method -explained below. It basically tells Flask to think we are handling a -request even though we are not, we are in an interactive Python shell. -Have a look at the explanation below: :ref:`context-locals`.) +(This also uses the :meth:`~flask.Flask.test_request_context` method, explained +below. It tells Flask to behave as though it is handling a request, even +though were are interacting with it through a Python shell. Have a look at the +explanation below. :ref:`context-locals`). -Why would you want to build URLs instead of hardcoding them in your +Why would you want to build URLs instead of hard-coding them into your templates? There are three good reasons for this: -1. reversing is often more descriptive than hardcoding the URLs. Also and - more importantly you can change URLs in one go without having to change - the URLs all over the place. +1. Reversing is often more descriptive than hard-coding the URLs. More + importantly, it allows you to change URLs in one go, without having to + remember to change URLs all over the place. 2. URL building will handle escaping of special characters and Unicode - data transparently for you, you don't have to deal with that. -3. If your application is placed outside the URL root (so say in - ``/myapplication`` instead of ``/``), :func:`~flask.url_for` will - handle that properly for you. + data transparently for you, so you don't have to deal with them. +3. If your application is placed outside the URL root (say, in + ``/myapplication`` instead of ``/``), :func:`~flask.url_for` will handle + that properly for you. HTTP Methods ```````````` -HTTP (the protocol web applications are speaking) knows different methods -to access URLs. By default a route only answers to `GET` requests, but -that can be changed by providing the `methods` argument to the +HTTP (the protocol web applications are speaking) knows different methods for +accessing URLs. By default, a route only answers to `GET` requests, but that +can be changed by providing the `methods` argument to the :meth:`~flask.Flask.route` decorator. Here are some examples:: @app.route('/login', methods=['GET', 'POST']) From ed40eacf423b9a182f3ca75287ddba9a5eb77801 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 21:45:19 -0500 Subject: [PATCH 0920/3747] Add tiny doc touchups. --- docs/installation.rst | 2 +- docs/quickstart.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index f02b5cb1..46f43186 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -77,7 +77,7 @@ corresponding environment. On OS X and Linux, do the following:: (Note the space between the dot and the script name. The dot means that this script should run in the context of the current shell. If this command does -not work in your shell, try replacing the dot with ``source``) +not work in your shell, try replacing the dot with ``source``.) If you are a Windows user, the following command is for you:: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2404361d..368bd96c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -122,7 +122,7 @@ Screenshot of the debugger in action: * ``use_debugger`` - whether to use the internal Flask debugger * ``use_reloader`` - whether to reload and fork the process on exception - ``debug`` must be True (i.e., exceptions must caught) in order for the + ``debug`` must be True (i.e., exceptions must be caught) in order for the other two options to have any value. If you're using Aptana/Eclipse for debugging you'll need to set both From b9907b496911d0d8676225151a36c28fe4f4b72f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 22:22:04 -0500 Subject: [PATCH 0921/3747] Expand get_flashed_messages tests. Ready to pull request #336. http://github.com/mitsuhiko/flask/pull/336 --- flask/testsuite/basic.py | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index a11f7806..5a41d03e 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -321,22 +321,61 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/test') def test(): + messages = flask.get_flashed_messages() + self.assert_equal(len(messages), 3) + self.assert_equal(messages[0], u'Hello World') + self.assert_equal(messages[1], u'Hello World') + self.assert_equal(messages[2], flask.Markup(u'Testing')) + return '' + + @app.route('/test_with_categories') + def test_with_categories(): messages = flask.get_flashed_messages(with_categories=True) self.assert_equal(len(messages), 3) self.assert_equal(messages[0], ('message', u'Hello World')) self.assert_equal(messages[1], ('error', u'Hello World')) self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) return '' - messages = flask.get_flashed_messages() - self.assert_equal(len(messages), 3) + + @app.route('/test_filter') + def test_filter(): + messages = flask.get_flashed_messages(category_filter=['message'], with_categories=True) + self.assert_equal(len(messages), 1) + self.assert_equal(messages[0], ('message', u'Hello World')) + return '' + + @app.route('/test_filters') + def test_filters(): + messages = flask.get_flashed_messages(category_filter=['message', 'warning'], with_categories=True) + self.assert_equal(len(messages), 2) + self.assert_equal(messages[0], ('message', u'Hello World')) + self.assert_equal(messages[1], ('warning', flask.Markup(u'Testing'))) + return '' + + @app.route('/test_filters_without_returning_categories') + def test_filters(): + messages = flask.get_flashed_messages(category_filter=['message', 'warning']) + self.assert_equal(len(messages), 2) self.assert_equal(messages[0], u'Hello World') - self.assert_equal(messages[1], u'Hello World') - self.assert_equal(messages[2], flask.Markup(u'Testing')) + self.assert_equal(messages[1], flask.Markup(u'Testing')) + return '' c = app.test_client() - c.get('/') + c.get('/') # Flash some messages. c.get('/test') + c.get('/') # Flash more messages. + c.get('/test_with_categories') + + c.get('/') # Flash more messages. + c.get('/test_filter') + + c.get('/') # Flash more messages. + c.get('/test_filters') + + c.get('/') # Flash more messages. + c.get('/test_filters_without_returning_categories') + def test_request_processing(self): app = flask.Flask(__name__) evts = [] From 676b3a4c13986f5743b8e6f3fa4d7c6cc2a401a4 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 22:44:06 -0500 Subject: [PATCH 0922/3747] Check status code in test client or fail silently. --- flask/testsuite/basic.py | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 5a41d03e..f543ba9f 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -319,7 +319,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.flash(flask.Markup(u'Testing'), 'warning') return '' - @app.route('/test') + @app.route('/test/') def test(): messages = flask.get_flashed_messages() self.assert_equal(len(messages), 3) @@ -328,7 +328,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(messages[2], flask.Markup(u'Testing')) return '' - @app.route('/test_with_categories') + @app.route('/test_with_categories/') def test_with_categories(): messages = flask.get_flashed_messages(with_categories=True) self.assert_equal(len(messages), 3) @@ -337,14 +337,14 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(messages[2], ('warning', flask.Markup(u'Testing'))) return '' - @app.route('/test_filter') + @app.route('/test_filter/') def test_filter(): messages = flask.get_flashed_messages(category_filter=['message'], with_categories=True) self.assert_equal(len(messages), 1) self.assert_equal(messages[0], ('message', u'Hello World')) return '' - @app.route('/test_filters') + @app.route('/test_filters/') def test_filters(): messages = flask.get_flashed_messages(category_filter=['message', 'warning'], with_categories=True) self.assert_equal(len(messages), 2) @@ -352,7 +352,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(messages[1], ('warning', flask.Markup(u'Testing'))) return '' - @app.route('/test_filters_without_returning_categories') + @app.route('/test_filters_without_returning_categories/') def test_filters(): messages = flask.get_flashed_messages(category_filter=['message', 'warning']) self.assert_equal(len(messages), 2) @@ -360,21 +360,33 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(messages[1], flask.Markup(u'Testing')) return '' + # Note: if status code assertions are missing, failed tests still pass. + # + # Since app.test_client() does not set debug=True, the AssertionErrors + # in the view functions are swallowed and the only indicator is a 500 + # status code. + # + # Also, create new test client on each test to clean flashed messages. + c = app.test_client() - c.get('/') # Flash some messages. - c.get('/test') + c.get('/') + assert c.get('/test/').status_code == 200 - c.get('/') # Flash more messages. - c.get('/test_with_categories') + c = app.test_client() + c.get('/') + assert c.get('/test_with_categories/').status_code == 200 - c.get('/') # Flash more messages. - c.get('/test_filter') + c = app.test_client() + c.get('/') + assert c.get('/test_filter/').status_code == 200 - c.get('/') # Flash more messages. - c.get('/test_filters') + c = app.test_client() + c.get('/') + assert c.get('/test_filters/').status_code == 200 - c.get('/') # Flash more messages. - c.get('/test_filters_without_returning_categories') + c = app.test_client() + c.get('/') + assert c.get('/test_filters_without_returning_categories/').status_code == 200 def test_request_processing(self): app = flask.Flask(__name__) From fa069f94dec3aeec4a81a00e7bbd8d95e400bf5f Mon Sep 17 00:00:00 2001 From: Steven Osborn Date: Sun, 16 Oct 2011 19:44:37 -0700 Subject: [PATCH 0923/3747] Allow category filtering in get_flashed_messages to allow rending categories in separate html blocks --- docs/patterns/flashing.rst | 23 +++++++++++++++++++++++ flask/helpers.py | 8 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index 7abe7165..be8fcfe8 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -117,3 +117,26 @@ categories. The loop looks slightly different in that situation then: This is just one example of how to render these flashed messages. One might also use the category to add a prefix such as ``Error:`` to the message. + +Filtering Flash Messages +------------------------ + +.. versionadded:: 0.9 + +Optionally you can pass a list of categories which filters the results of +:func:`~flask.get_flashed_messages`. This is useful if you wish to +render each category in a separate block. + +.. sourcecode:: html+jinja + +{% with errors = get_flashed_messages(category_filter=["error"]) %} + {% if errors %} +

    +
      + {% for message in messages %} +
    • {{ message }}
    • + {% endfor %} +
    +
    + {% endif %} +{% endwith %} \ No newline at end of file diff --git a/flask/helpers.py b/flask/helpers.py index 3c9a5669..aa813003 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -264,7 +264,7 @@ def flash(message, category='message'): session.setdefault('_flashes', []).append((category, message)) -def get_flashed_messages(with_categories=False): +def get_flashed_messages(with_categories=False, category_filter=[]): """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return the same messages. By default just the messages are returned, @@ -282,12 +282,18 @@ def get_flashed_messages(with_categories=False): .. versionchanged:: 0.3 `with_categories` parameter added. + .. versionchanged: 0.9 + `category_filter` parameter added. + :param with_categories: set to `True` to also receive categories. + :param category_filter: whitelist of categories to limit return values """ flashes = _request_ctx_stack.top.flashes if flashes is None: _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ if '_flashes' in session else [] + if category_filter: + flashes = filter(lambda f: f[0] in category_filter, flashes) if not with_categories: return [x[1] for x in flashes] return flashes From 81010bf7afb786d3a19ddc2469f9bfdcc9e0d194 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 23:10:21 -0500 Subject: [PATCH 0924/3747] Add get_flashed_messages to CHANGES, expand docs. --- CHANGES | 4 +++- flask/helpers.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 6f360814..9a712afc 100644 --- a/CHANGES +++ b/CHANGES @@ -34,7 +34,9 @@ Relase date to be decided, codename to be chosen. custom template filters application wide, :meth:`flask.Flask.add_template_filter` and :meth:`flask.Blueprint.add_app_template_filter`. - +- The :func:`flask.get_flashed_messages` function now allows rendering flashed + message categories in separate blocks, through a ``category_filter`` + argument. Version 0.8.1 ------------- diff --git a/flask/helpers.py b/flask/helpers.py index aa813003..56c59199 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -251,7 +251,7 @@ def flash(message, category='message'): flashed message from the session and to display it to the user, the template has to call :func:`get_flashed_messages`. - .. versionchanged: 0.3 + .. versionchanged:: 0.3 `category` parameter added. :param message: the message to be flashed. @@ -271,6 +271,16 @@ def get_flashed_messages(with_categories=False, category_filter=[]): but when `with_categories` is set to `True`, the return value will be a list of tuples in the form ``(category, message)`` instead. + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (`True` gives a tuple, where `False` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + Example usage: .. sourcecode:: html+jinja @@ -279,10 +289,27 @@ def get_flashed_messages(with_categories=False, category_filter=[]):

    {{ msg }} {% endfor %} + Example usage similar to http://twitter.github.com/bootstrap/#alerts: + + .. sourcecode:: html+jinja + + {% with errors = get_flashed_messages(category_filter=["error"]) %} + {% if errors %} +

    + × +
      + {%- for msg in errors %} +
    • {{ msg }}
    • + {% endfor -%} +
    +
    + {% endif %} + {% endwith %} + .. versionchanged:: 0.3 `with_categories` parameter added. - .. versionchanged: 0.9 + .. versionchanged:: 0.9 `category_filter` parameter added. :param with_categories: set to `True` to also receive categories. From c93ea5551c6928745414f186ef465e2699a6833b Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 23:16:43 -0500 Subject: [PATCH 0925/3747] Keep flashed message examples in one place. --- docs/patterns/flashing.rst | 23 ++++++++++++----------- flask/helpers.py | 25 +------------------------ 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index be8fcfe8..f3d80d32 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -129,14 +129,15 @@ render each category in a separate block. .. sourcecode:: html+jinja -{% with errors = get_flashed_messages(category_filter=["error"]) %} - {% if errors %} -
    -
      - {% for message in messages %} -
    • {{ message }}
    • - {% endfor %} -
    -
    - {% endif %} -{% endwith %} \ No newline at end of file + {% with errors = get_flashed_messages(category_filter=["error"]) %} + {% if errors %} +
    + × +
      + {%- for msg in errors %} +
    • {{ msg }}
    • + {% endfor -%} +
    +
    + {% endif %} + {% endwith %} diff --git a/flask/helpers.py b/flask/helpers.py index 56c59199..25250d26 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -281,30 +281,7 @@ def get_flashed_messages(with_categories=False, category_filter=[]): * `category_filter` filters the messages down to only those matching the provided categories. - Example usage: - - .. sourcecode:: html+jinja - - {% for category, msg in get_flashed_messages(with_categories=true) %} -

    {{ msg }} - {% endfor %} - - Example usage similar to http://twitter.github.com/bootstrap/#alerts: - - .. sourcecode:: html+jinja - - {% with errors = get_flashed_messages(category_filter=["error"]) %} - {% if errors %} -

    - × -
      - {%- for msg in errors %} -
    • {{ msg }}
    • - {% endfor -%} -
    -
    - {% endif %} - {% endwith %} + See :ref:`message-flashing-pattern` for examples. .. versionchanged:: 0.3 `with_categories` parameter added. From 46e7bc70833e803657726aaff7c63ca4f4865c79 Mon Sep 17 00:00:00 2001 From: Joe Esposito Date: Mon, 24 Oct 2011 17:46:35 -0400 Subject: [PATCH 0926/3747] In Flask.run, now when the host/port argument is None, it will use its default value. --- flask/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flask/app.py b/flask/app.py index 0e462020..b70162c1 100644 --- a/flask/app.py +++ b/flask/app.py @@ -695,6 +695,10 @@ class Flask(_PackageBoundObject): information. """ from werkzeug.serving import run_simple + if host is None: + host = '127.0.0.1' + if port is None: + port = 5000 if debug is not None: self.debug = bool(debug) options.setdefault('use_reloader', self.debug) From 19d32cb17285e2614d8aecbf4b0f63cfdac629cf Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 23:33:13 -0500 Subject: [PATCH 0927/3747] Update Flask.run signature, note defaults. --- flask/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/flask/app.py b/flask/app.py index b70162c1..26519d40 100644 --- a/flask/app.py +++ b/flask/app.py @@ -664,7 +664,7 @@ class Flask(_PackageBoundObject): # existing views. context.update(orig_ctx) - def run(self, host='127.0.0.1', port=5000, debug=None, **options): + def run(self, host=None, port=None, debug=None, **options): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. @@ -684,9 +684,10 @@ class Flask(_PackageBoundObject): won't catch any exceptions because there won't be any to catch. - :param host: the hostname to listen on. set this to ``'0.0.0.0'`` - to have the server available externally as well. - :param port: the port of the webserver + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'``. + :param port: the port of the webserver. Defaults to ``5000``. :param debug: if given, enable or disable debug mode. See :attr:`debug`. :param options: the options to be forwarded to the underlying From 234ac198cb9d3955d19d3ebe74cbbd91a2d4bb92 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Mon, 16 Jan 2012 23:38:36 -0500 Subject: [PATCH 0928/3747] Add updated Flask.run to CHANGES. --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index 9a712afc..970f183a 100644 --- a/CHANGES +++ b/CHANGES @@ -37,6 +37,12 @@ Relase date to be decided, codename to be chosen. - The :func:`flask.get_flashed_messages` function now allows rendering flashed message categories in separate blocks, through a ``category_filter`` argument. +- The :meth:`flask.Flask.run` method now accepts `None` for `host` and `port` + arguments, using default values when `None`. This allows for calling run + using configuration values, e.g. ``app.run(app.config.get('MYHOST'), + app.config.get('MYPORT'))``, with proper behavior whether or not a config + file is provided. + Version 0.8.1 ------------- From 49b77fbc7aa826efbf0b58d6973ef02c50e8fc66 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 17 Jan 2012 11:38:00 -0500 Subject: [PATCH 0929/3747] Add missing colons to versionadded. --- flask/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 26519d40..ccce7046 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1120,7 +1120,7 @@ class Flask(_PackageBoundObject): registered error handlers and fall back to returning the exception as response. - .. versionadded: 0.3 + .. versionadded:: 0.3 """ handlers = self.error_handler_spec.get(request.blueprint) if handlers and e.code in handlers: @@ -1189,7 +1189,7 @@ class Flask(_PackageBoundObject): for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. - .. versionadded: 0.3 + .. versionadded:: 0.3 """ exc_type, exc_value, tb = sys.exc_info() From 2e5de9829774ca10682b298544bf3ce2bf61bab7 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 17 Jan 2012 19:33:48 -0500 Subject: [PATCH 0930/3747] Use app.testing=True for asserts in messages test. --- flask/testsuite/basic.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index f543ba9f..e6a278e5 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -309,8 +309,15 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) def test_extended_flashing(self): + # Be sure app.testing=True below, else tests can fail silently. + # + # Specifically, if app.testing is not set to True, the AssertionErrors + # in the view functions will cause a 500 response to the test client + # instead of propagating exceptions. + app = flask.Flask(__name__) app.secret_key = 'testkey' + app.testing = True @app.route('/') def index(): @@ -360,33 +367,27 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(messages[1], flask.Markup(u'Testing')) return '' - # Note: if status code assertions are missing, failed tests still pass. - # - # Since app.test_client() does not set debug=True, the AssertionErrors - # in the view functions are swallowed and the only indicator is a 500 - # status code. - # - # Also, create new test client on each test to clean flashed messages. + # Create new test client on each test to clean flashed messages. c = app.test_client() c.get('/') - assert c.get('/test/').status_code == 200 + c.get('/test/') c = app.test_client() c.get('/') - assert c.get('/test_with_categories/').status_code == 200 + c.get('/test_with_categories/') c = app.test_client() c.get('/') - assert c.get('/test_filter/').status_code == 200 + c.get('/test_filter/') c = app.test_client() c.get('/') - assert c.get('/test_filters/').status_code == 200 + c.get('/test_filters/') c = app.test_client() c.get('/') - assert c.get('/test_filters_without_returning_categories/').status_code == 200 + c.get('/test_filters_without_returning_categories/') def test_request_processing(self): app = flask.Flask(__name__) From 56177bcbd16f9e73252d7af7c78d25dc60ed946d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 17 Jan 2012 19:43:11 -0500 Subject: [PATCH 0931/3747] Document app.testing=True for test client, #381. --- flask/app.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flask/app.py b/flask/app.py index ccce7046..15e432de 100644 --- a/flask/app.py +++ b/flask/app.py @@ -716,6 +716,17 @@ class Flask(_PackageBoundObject): """Creates a test client for this application. For information about unit testing head over to :ref:`testing`. + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + The test client can be used in a `with` block to defer the closing down of the context until the end of the `with` block. This is useful if you want to access the context locals for testing:: From b786eac5574e478d51314333fb6309456bff7b76 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 18 Jan 2012 18:57:05 -0500 Subject: [PATCH 0932/3747] Add test for limited imp loaders, #380. --- flask/testsuite/config.py | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index e0f7005f..00f77cea 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -8,10 +8,14 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os import sys import flask +import pkgutil import unittest +from contextlib import contextmanager from flask.testsuite import FlaskTestCase @@ -84,6 +88,35 @@ class ConfigTestCase(FlaskTestCase): self.assert_equal(app.permanent_session_lifetime.seconds, 42) +class LimitedLoaderMockWrapper(object): + def __init__(self, loader): + self.loader = loader + + def __getattr__(self, name): + if name in ('archive', 'get_filename'): + msg = 'Mocking a loader which does not have `%s.`' % name + raise AttributeError, msg + return getattr(self.loader, name) + + +@contextmanager +def patch_pkgutil_get_loader(wrapper_class=LimitedLoaderMockWrapper): + """Patch pkgutil.get_loader to give loader without get_filename or archive. + + This provides for tests where a system has custom loaders, e.g. Google App + Engine's HardenedModulesHook, which have neither the `get_filename` method + nor the `archive` attribute. + """ + old_get_loader = pkgutil.get_loader + def get_loader(*args, **kwargs): + return wrapper_class(old_get_loader(*args, **kwargs)) + try: + pkgutil.get_loader = get_loader + yield + finally: + pkgutil.get_loader = old_get_loader + + class InstanceTestCase(FlaskTestCase): def test_explicit_instance_paths(self): @@ -133,6 +166,24 @@ class InstanceTestCase(FlaskTestCase): if 'site_app' in sys.modules: del sys.modules['site_app'] + def test_installed_module_paths_with_limited_loader(self): + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + site_packages = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages') + sys.path.append(site_packages) + with patch_pkgutil_get_loader(): + try: + import site_app + self.assert_equal(site_app.app.instance_path, + os.path.join(expected_prefix, 'var', + 'site_app-instance')) + finally: + sys.prefix = real_prefix + sys.path.remove(site_packages) + if 'site_app' in sys.modules: + del sys.modules['site_app'] + def test_installed_package_paths(self): here = os.path.abspath(os.path.dirname(__file__)) expected_prefix = os.path.join(here, 'test_apps') @@ -150,6 +201,24 @@ class InstanceTestCase(FlaskTestCase): if 'installed_package' in sys.modules: del sys.modules['installed_package'] + def test_installed_package_paths_with_limited_loader(self): + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + installed_path = os.path.join(expected_prefix, 'path') + sys.path.append(installed_path) + with patch_pkgutil_get_loader(): + try: + import installed_package + self.assert_equal(installed_package.app.instance_path, + os.path.join(expected_prefix, 'var', + 'installed_package-instance')) + finally: + sys.prefix = real_prefix + sys.path.remove(installed_path) + if 'installed_package' in sys.modules: + del sys.modules['installed_package'] + def test_prefix_package_paths(self): here = os.path.abspath(os.path.dirname(__file__)) expected_prefix = os.path.join(here, 'test_apps') @@ -167,6 +236,24 @@ class InstanceTestCase(FlaskTestCase): if 'site_package' in sys.modules: del sys.modules['site_package'] + def test_prefix_package_paths_with_limited_loader(self): + here = os.path.abspath(os.path.dirname(__file__)) + expected_prefix = os.path.join(here, 'test_apps') + real_prefix, sys.prefix = sys.prefix, expected_prefix + site_packages = os.path.join(expected_prefix, 'lib', 'python2.5', 'site-packages') + sys.path.append(site_packages) + with patch_pkgutil_get_loader(): + try: + import site_package + self.assert_equal(site_package.app.instance_path, + os.path.join(expected_prefix, 'var', + 'site_package-instance')) + finally: + sys.prefix = real_prefix + sys.path.remove(site_packages) + if 'site_package' in sys.modules: + del sys.modules['site_package'] + def test_egg_installed_paths(self): here = os.path.abspath(os.path.dirname(__file__)) expected_prefix = os.path.join(here, 'test_apps') From 76c1a1f7227ca13f3c5952b248af93b75e9a95c9 Mon Sep 17 00:00:00 2001 From: FND Date: Mon, 23 Jan 2012 20:12:56 +0100 Subject: [PATCH 0933/3747] fixed spelling of "instantiate" while the interwebs suggest "instanciate" might be a valid spelling, it seems quite uncommon and potentially irritating (to pedants like myself) --- docs/patterns/appdispatch.rst | 2 +- docs/views.rst | 2 +- flask/views.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 93b4af96..177ade2b 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -58,7 +58,7 @@ Dispatch by Subdomain Sometimes you might want to use multiple instances of the same application with different configurations. Assuming the application is created inside -a function and you can call that function to instanciate it, that is +a function and you can call that function to instantiate it, that is really easy to implement. In order to develop your application to support creating new instances in functions have a look at the :ref:`app-factories` pattern. diff --git a/docs/views.rst b/docs/views.rst index feee0a8b..9270921b 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -74,7 +74,7 @@ enough to explain the basic principle. When you have a class based view the question comes up what `self` points to. The way this works is that whenever the request is dispatched a new instance of the class is created and the :meth:`~flask.views.View.dispatch_request` method is called with -the parameters from the URL rule. The class itself is instanciated with +the parameters from the URL rule. The class itself is instantiated with the parameters passed to the :meth:`~flask.views.View.as_view` function. For instance you can write a class like this:: diff --git a/flask/views.py b/flask/views.py index 3e35e46f..811fa196 100644 --- a/flask/views.py +++ b/flask/views.py @@ -72,7 +72,7 @@ class View(object): def as_view(cls, name, *class_args, **class_kwargs): """Converts the class into an actual view function that can be used with the routing system. What it does internally is generating - a function on the fly that will instanciate the :class:`View` + a function on the fly that will instantiate the :class:`View` on each request and call the :meth:`dispatch_request` method on it. The arguments passed to :meth:`as_view` are forwarded to the @@ -90,7 +90,7 @@ class View(object): # we attach the view class to the view function for two reasons: # first of all it allows us to easily figure out what class based - # view this thing came from, secondly it's also used for instanciating + # view this thing came from, secondly it's also used for instantiating # the view class so you can actually replace it with something else # for testing purposes and debugging. view.view_class = cls From 2792dcf23e848472767a075e7c30e28890fd539d Mon Sep 17 00:00:00 2001 From: FND Date: Mon, 23 Jan 2012 20:27:42 +0100 Subject: [PATCH 0934/3747] simplified as_view documentation in the process, rewrapped lines to 78 chars (the file's current maximum) --- flask/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask/views.py b/flask/views.py index 811fa196..150e84c3 100644 --- a/flask/views.py +++ b/flask/views.py @@ -70,10 +70,10 @@ class View(object): @classmethod def as_view(cls, name, *class_args, **class_kwargs): - """Converts the class into an actual view function that can be - used with the routing system. What it does internally is generating - a function on the fly that will instantiate the :class:`View` - on each request and call the :meth:`dispatch_request` method on it. + """Converts the class into an actual view function that can be used + with the routing system. Internally this generates a function on the + fly which will instantiate the :class:`View` on each request and call + the :meth:`dispatch_request` method on it. The arguments passed to :meth:`as_view` are forwarded to the constructor of the class. From c5ebf9a97d0443bff59ff6a37aee8afd870ceabb Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Tue, 24 Jan 2012 16:48:04 -0500 Subject: [PATCH 0935/3747] Added PATCH method to the list of HTTP method functions for use in the flask.views.MethodView class. --- flask/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index 150e84c3..2aaaf156 100644 --- a/flask/views.py +++ b/flask/views.py @@ -12,7 +12,7 @@ from .globals import request http_method_funcs = frozenset(['get', 'post', 'head', 'options', - 'delete', 'put', 'trace']) + 'delete', 'put', 'trace', 'patch']) class View(object): From 92dbe3153a9e0c007ecd1987ccd5f3d796c901af Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 24 Jan 2012 18:00:14 -0500 Subject: [PATCH 0936/3747] Export .epub with docs, #388. --- Makefile | 3 ++- docs/_templates/sidebarintro.html | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 43f47275..b7094ce6 100644 --- a/Makefile +++ b/Makefile @@ -23,12 +23,13 @@ clean-pyc: find . -name '*~' -exec rm -f {} + upload-docs: - $(MAKE) -C docs html dirhtml latex + $(MAKE) -C docs html dirhtml latex epub $(MAKE) -C docs/_build/latex all-pdf cd docs/_build/; mv html flask-docs; zip -r flask-docs.zip flask-docs; mv flask-docs html rsync -a docs/_build/dirhtml/ pocoo.org:/var/www/flask.pocoo.org/docs/ rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip + rsync -a docs/_build/epub/Flask.epub pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.epub docs: $(MAKE) -C docs html diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 850fe86a..76777225 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -9,6 +9,7 @@

    Useful Links

    From 36f3184396f72b106911f497ce43c3415cb4f551 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 26 Jan 2012 13:24:15 -0500 Subject: [PATCH 0937/3747] Automate .mobi generation of docs, #388. --- Makefile | 6 ++++++ docs/_templates/sidebarintro.html | 1 + 2 files changed, 7 insertions(+) diff --git a/Makefile b/Makefile index b7094ce6..6fa48693 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ clean-pyc: find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + +# ebook-convert docs: http://manual.calibre-ebook.com/cli/ebook-convert.html upload-docs: $(MAKE) -C docs html dirhtml latex epub $(MAKE) -C docs/_build/latex all-pdf @@ -30,6 +31,11 @@ upload-docs: rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip rsync -a docs/_build/epub/Flask.epub pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.epub + @echo 'Building .mobi from .epub...' + @echo 'Command `ebook-covert` is provided by calibre package.' + @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' + @echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' + ssh -X pocoo.org ebook-convert /var/www/flask.pocoo.org/docs/flask-docs.epub /var/www/flask.pocoo.org/docs/flask-docs.mobi --cover http://flask.pocoo.org/docs/_images/logo-full.png --authors 'Armin Ronacher' docs: $(MAKE) -C docs html diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 76777225..26e32261 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -10,6 +10,7 @@

    Useful Links

    From 5cb50a46eeb29dddacf21d0348050e02ff96c3b5 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 31 Jan 2012 05:46:18 -0500 Subject: [PATCH 0938/3747] Fix Message Flashing doc, from SwashBuckla #pocoo. Provide a full example as promised in the doc. --- docs/patterns/flashing.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index f3d80d32..5f3b02eb 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -16,7 +16,11 @@ Simple Flashing So here is a full example:: - from flask import flash, redirect, url_for, render_template + from flask import Flask, flash, redirect, render_template, \ + request, url_for + + app = Flask(__name__) + app.secret_key = 'some_secret' @app.route('/') def index(): @@ -27,13 +31,17 @@ So here is a full example:: error = None if request.method == 'POST': if request.form['username'] != 'admin' or \ - request.form['password'] != 'secret': + request.form['password'] != 'secret': error = 'Invalid credentials' else: flash('You were successfully logged in') return redirect(url_for('index')) return render_template('login.html', error=error) + if __name__ == "__main__": + app.run() + + And here the ``layout.html`` template which does the magic: .. sourcecode:: html+jinja From 4aebc267bc67c5d8a1687c0e5a7ecc949d6e7d20 Mon Sep 17 00:00:00 2001 From: FND Date: Tue, 31 Jan 2012 13:54:46 +0100 Subject: [PATCH 0939/3747] Hyphenate "class-based" makes it more readable --- CHANGES | 4 ++-- docs/api.rst | 2 +- docs/extensiondev.rst | 4 ++-- docs/views.rst | 4 ++-- flask/views.py | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 970f183a..7188e57b 100644 --- a/CHANGES +++ b/CHANGES @@ -89,7 +89,7 @@ Released on September 29th 2011, codename Rakija variable as well as ``SERVER_NAME`` are now properly used by the test client as defaults. - Added :attr:`flask.views.View.decorators` to support simpler decorating of - pluggable (class based) views. + pluggable (class-based) views. - Fixed an issue where the test client if used with the "with" statement did not trigger the execution of the teardown handlers. - Added finer control over the session cookie parameters. @@ -177,7 +177,7 @@ Released on June 28th 2011, codename Grappa might occur during request processing (for instance database connection errors, timeouts from remote resources etc.). - Blueprints can provide blueprint specific error handlers. -- Implemented generic :ref:`views` (class based views). +- Implemented generic :ref:`views` (class-based views). Version 0.6.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index d2b62199..ec7e4f63 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -476,7 +476,7 @@ Signals .. _blinker: http://pypi.python.org/pypi/blinker -Class Based Views +Class-Based Views ----------------- .. versionadded:: 0.7 diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index ee0d5e60..074d06ab 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -148,7 +148,7 @@ classes: a remote application that uses OAuth. What to use depends on what you have in mind. For the SQLite 3 extension -we will use the class based approach because it will provide users with a +we will use the class-based approach because it will provide users with a manager object that handles opening and closing database connections. The Extension Code @@ -203,7 +203,7 @@ So here's what these lines of code do: 5. Finally, we add a `get_db` function that simplifies access to the context's database. -So why did we decide on a class based approach here? Because using our +So why did we decide on a class-based approach here? Because using our extension looks something like this:: from flask import Flask diff --git a/docs/views.rst b/docs/views.rst index 9270921b..02c62704 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -23,7 +23,7 @@ database and renders into a template:: This is simple and flexible, but if you want to provide this view in a generic fashion that can be adapted to other models and templates as well -you might want more flexibility. This is where pluggable class based +you might want more flexibility. This is where pluggable class-based views come into place. As the first step to convert this into a class based view you would do this:: @@ -70,7 +70,7 @@ this by itself is not helpful, so let's refactor the code a bit:: return User.query.all() This of course is not that helpful for such a small example, but it's good -enough to explain the basic principle. When you have a class based view +enough to explain the basic principle. When you have a class-based view the question comes up what `self` points to. The way this works is that whenever the request is dispatched a new instance of the class is created and the :meth:`~flask.views.View.dispatch_request` method is called with diff --git a/flask/views.py b/flask/views.py index 2aaaf156..79d62992 100644 --- a/flask/views.py +++ b/flask/views.py @@ -3,7 +3,7 @@ flask.views ~~~~~~~~~~~ - This module provides class based views inspired by the ones in Django. + This module provides class-based views inspired by the ones in Django. :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. @@ -50,7 +50,7 @@ class View(object): #: A for which methods this pluggable view can handle. methods = None - #: The canonical way to decorate class based views is to decorate the + #: The canonical way to decorate class-based views is to decorate the #: return value of as_view(). However since this moves parts of the #: logic from the class declaration to the place where it's hooked #: into the routing system. @@ -89,7 +89,7 @@ class View(object): view = decorator(view) # we attach the view class to the view function for two reasons: - # first of all it allows us to easily figure out what class based + # first of all it allows us to easily figure out what class-based # view this thing came from, secondly it's also used for instantiating # the view class so you can actually replace it with something else # for testing purposes and debugging. @@ -120,7 +120,7 @@ class MethodViewType(type): class MethodView(View): - """Like a regular class based view but that dispatches requests to + """Like a regular class-based view but that dispatches requests to particular methods. For instance if you implement a method called :meth:`get` it means you will response to ``'GET'`` requests and the :meth:`dispatch_request` implementation will automatically From d33f9990c80bd25fd36e19ea4d010fe0a1a56c42 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Wed, 1 Feb 2012 14:49:46 +0200 Subject: [PATCH 0940/3747] Document context processors' variable functions --- docs/templating.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/templating.rst b/docs/templating.rst index bd940b0e..15433f2a 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -186,3 +186,22 @@ The context processor above makes a variable called `user` available in the template with the value of `g.user`. This example is not very interesting because `g` is available in templates anyways, but it gives an idea how this works. + +It is also possible to inject functions that can have any number of +arguments:: + + @app.context_processor + def price_formatter(): + def loader(amount, currency=u'€'): + return u'{0:.2f}{1}.format(amount, currency) + return dict(format_price=loader) + +The above construct registers a "variable" function called +`format_price` which can then be used in template:: + + {{ format_price(0.33) }} + +The difference from regular context processor' variables is that functions +are evaluated upon template rendering compared to variables whose values +are created during `app` startup . Therefore "variable" functions make it +possible to inject dynamic data into templates. From 8ef2ca99b991f66f0b61975e883325fcf5c13046 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 1 Feb 2012 18:03:29 -0500 Subject: [PATCH 0941/3747] Reword context processors for functions. --- docs/templating.rst | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/templating.rst b/docs/templating.rst index 15433f2a..19217528 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -147,6 +147,8 @@ autoescape %}`` block: Whenever you do this, please be very cautious about the variables you are using in this block. +.. _registering-filters: + Registering Filters ------------------- @@ -176,7 +178,7 @@ context processors exist in Flask. Context processors run before the template is rendered and have the ability to inject new values into the template context. A context processor is a function that returns a dictionary. The keys and values of this dictionary are then merged with -the template context:: +the template context, for all templates in the app:: @app.context_processor def inject_user(): @@ -187,21 +189,21 @@ the template with the value of `g.user`. This example is not very interesting because `g` is available in templates anyways, but it gives an idea how this works. -It is also possible to inject functions that can have any number of -arguments:: +Variables are not limited to values; a context processor can also make +functions available to templates (since Python allows passing around +functions):: @app.context_processor - def price_formatter(): - def loader(amount, currency=u'€'): + def utility_processor(): + def format_price(amount, currency=u'€'): return u'{0:.2f}{1}.format(amount, currency) - return dict(format_price=loader) + return dict(format_price=format_price) -The above construct registers a "variable" function called -`format_price` which can then be used in template:: +The context processor above makes the `format_price` function available to all +templates:: {{ format_price(0.33) }} -The difference from regular context processor' variables is that functions -are evaluated upon template rendering compared to variables whose values -are created during `app` startup . Therefore "variable" functions make it -possible to inject dynamic data into templates. +You could also build `format_price` as a template filter (see +:ref:`registering-filters`), but this demonstrates how to pass functions in a +context processor. From 5a1bef4429f289eac52f8ccf57a1a8409898d489 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 1 Feb 2012 18:03:32 -0500 Subject: [PATCH 0942/3747] Demonstrate in docs how to use registered filters. Requested by @plaes on #pocoo irc. --- docs/templating.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/templating.rst b/docs/templating.rst index 19217528..d4878cdb 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -168,7 +168,13 @@ The two following examples work the same and both reverse an object:: app.jinja_env.filters['reverse'] = reverse_filter In case of the decorator the argument is optional if you want to use the -function name as name of the filter. +function name as name of the filter. Once registered, you can use the filter +in your templates in the same way as Jinja2's builtin filters, for example if +you have a Python list in context called `mylist`:: + + {% for x in mylist | reverse %} + {% endfor %} + Context Processors ------------------ From dfd3ef6d5460d666226d1831de48cdaff72c1bb6 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 3 Feb 2012 13:19:04 -0500 Subject: [PATCH 0943/3747] Add cookie size limit note to sessions section. Result of discussion with jujule_ on #pocoo irc. --- docs/quickstart.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 368bd96c..1d524a09 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -798,6 +798,13 @@ not using the template engine (as in this example). Just take that thing and copy/paste it into your code and you're done. +A note on cookie-based sessions: Flask will take the values you put into the +session object and serialize them into a cookie. If you are finding some +values do not persist across requests, cookies are indeed enabled, and you are +not getting a clear error message, check the size of the cookie in your page +responses compared to the size supported by web browsers. + + Message Flashing ---------------- From 69e7a0a2a0391ed1bdd102deffbd98137790f959 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 3 Feb 2012 18:11:14 -0500 Subject: [PATCH 0944/3747] Move debugger details into a new section, #343. --- docs/errorhandling.rst | 69 ++++++++++++++++++++++++++++++++++++++++-- docs/quickstart.rst | 39 ++---------------------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index debb9d75..97ff4df2 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -1,7 +1,7 @@ .. _application-errors: -Handling Application Errors -=========================== +Logging Application Errors +========================== .. versionadded:: 0.3 @@ -235,3 +235,68 @@ iterating over them to attach handlers:: for logger in loggers: logger.addHandler(mail_handler) logger.addHandler(file_handler) + + +Debugging Application Errors +============================ + +For production applications, configure your application with logging and +notifications as described in :ref:`application-errors`. This section provides +pointers when debugging deployment configuration and digging deeper with a +full-featured Python debugger. + + +When in Doubt, Run Manually +--------------------------- + +Having problems getting your application configured for production? If you +have shell access to your host, verify that you can run your application +manually from the shell in the deployment environment. Be sure to run under +the same user account as the configured deployment to troubleshoot permission +issues. You can use Flask's builtin development server with `debug=True` on +your production host, which is helpful in catching configuration issues, but +**be sure to do this temporarily in a controlled environment.** Do not run in +production with `debug=True`. + + +.. _working-with-debuggers: + +Working with Debuggers +---------------------- + +To dig deeper, possibly to trace code execution, Flask provides a debugger out +of the box (see :ref:`debug-mode`). If you would like to use another Python +debugger, note that debuggers interfere with each other. You have to set some +options in order to use your favorite debugger: + +* ``debug`` - whether to enable debug mode and catch exceptinos +* ``use_debugger`` - whether to use the internal Flask debugger +* ``use_reloader`` - whether to reload and fork the process on exception + +``debug`` must be True (i.e., exceptions must be caught) in order for the other +two options to have any value. + +If you're using Aptana/Eclipse for debugging you'll need to set both +``use_debugger`` and ``use_reloader`` to False. + +A possible useful pattern for configuration is to set the following in your +config.yaml (change the block as approriate for your application, of course):: + + FLASK: + DEBUG: True + DEBUG_WITH_APTANA: True + +Then in your application's entry-point (main.py), you could have something like:: + + if __name__ == "__main__": + # To allow aptana to receive errors, set use_debugger=False + app = create_app(config="config.yaml") + + if app.debug: use_debugger = True + try: + # Disable Flask's debugger if external debugger is requested + use_debugger = not(app.config.get('DEBUG_WITH_APTANA')) + except: + pass + app.run(use_debugger=use_debugger, debug=app.debug, + use_reloader=use_debugger, host='0.0.0.0') diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1d524a09..9fde0c2f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -77,6 +77,8 @@ To stop the server, hit control-C. This tells your operating system to listen on all public IPs. +.. _debug-mode: + Debug Mode ---------- @@ -112,42 +114,7 @@ Screenshot of the debugger in action: :class: screenshot :alt: screenshot of debugger in action -.. admonition:: Working With Other Debuggers - - Debuggers interfere with each other. - That said, you may still wish to use the debugger in a tool of your choice. - Flask provides the following options to manage the debug process: - - * ``debug`` - whether to enable debug mode and catch exceptinos - * ``use_debugger`` - whether to use the internal Flask debugger - * ``use_reloader`` - whether to reload and fork the process on exception - - ``debug`` must be True (i.e., exceptions must be caught) in order for the - other two options to have any value. - - If you're using Aptana/Eclipse for debugging you'll need to set both - ``use_debugger`` and ``use_reloader`` to False. - - A possible useful pattern for configuration is to set the following in your - config.yaml (change the block as approriate for your application, of course):: - - FLASK: - DEBUG: True - DEBUG_WITH_APTANA: True - - Then in your application's entry-point (main.py), you could have something like:: - - if __name__ == "__main__": - # To allow aptana to receive errors, set use_debugger=False - app = create_app(config="config.yaml") - - if app.debug: use_debugger = True - try: - # Disable Flask's debugger if external debugger is requested - use_debugger = not(app.config.get('DEBUG_WITH_APTANA')) - except: - pass - app.run(use_debugger=use_debugger, debug=app.debug, use_reloader=use_debugger, host='0.0.0.0') +Have another debugger in mind? See :ref:`working-with-debuggers`. Routing From 84b96ac9d398afd5a620c48cfe9e969b95e6577a Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 4 Feb 2012 10:37:58 -0500 Subject: [PATCH 0945/3747] Add pypy to tox.ini. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 15911b4c..d9db8990 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py25,py26,py27 +envlist=py25,py26,py27,pypy [testenv] commands=make test From 96d7f207878bcd561c4085312eb9e235b9dc4d2d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sat, 4 Feb 2012 10:44:16 -0500 Subject: [PATCH 0946/3747] Fix tox warning, test "not installed in testenv". --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d9db8990..91c6d664 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,4 @@ envlist=py25,py26,py27,pypy [testenv] -commands=make test +commands=python run-tests.py From 3de8de1985f46297243eab340abd6c45c82bb9c4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 6 Feb 2012 20:19:32 -0500 Subject: [PATCH 0947/3747] pip > easy_install --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d41a3eca..3a302cea 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ And Easy to Setup :: - $ easy_install Flask + $ pip install Flask $ python hello.py * Running on http://localhost:5000/ From 4a75198f36625b472215a4ef6192c41f950fa202 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 6 Feb 2012 20:28:17 -0500 Subject: [PATCH 0948/3747] pip and distribute installation --- docs/installation.rst | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 46f43186..0d3fcd74 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -100,7 +100,7 @@ System-Wide Installation This is possible as well, though I do not recommend it. Just run `easy_install` with root privileges:: - $ sudo easy_install Flask + $ sudo pip install Flask (On Windows systems, run it in a command-prompt window with administrator privleges, and leave out `sudo`.) @@ -110,7 +110,7 @@ Living on the Edge ------------------ If you want to work with the latest version of Flask, there are two ways: you -can either let `easy_install` pull in the development version, or you can tell +can either let `pip` pull in the development version, or you can tell it to operate on a git checkout. Either way, virtualenv is recommended. Get the git checkout in a new virtualenv and run in development mode:: @@ -138,19 +138,19 @@ To just get the development version without git, do this instead:: $ . env/bin/activate New python executable in env/bin/python Installing setuptools............done. - $ easy_install Flask==dev + $ pip install Flask==dev ... Finished processing dependencies for Flask==dev .. _windows-easy-install: -`easy_install` on Windows -------------------------- +`pip` and `distribute` on Windows +----------------------------------- On Windows, installation of `easy_install` is a little bit trickier, but still -quite easy. The easiest way to do it is to download the `ez_setup.py`_ file -and run it. The easiest way to run the file is to open your downloads folder -and double-click on the file. +quite easy. The easiest way to do it is to download the +`distribute_setup.py`_ file and run it. The easiest way to run the file is to +open your downloads folder and double-click on the file. Next, add the `easy_install` command and other Python scripts to the command search path, by adding your Python installation's Scripts folder @@ -161,14 +161,18 @@ Then click on "Advanced System settings" (in Windows XP, click on the Finally, double-click on the "Path" variable in the "System variables" section, and add the path of your Python interpreter's Scripts folder. Be sure to delimit it from existing values with a semicolon. Assuming you are using -Python 2.6 on the default path, add the following value:: +Python 2.7 on the default path, add the following value:: - ;C:\Python26\Scripts + ;C:\Python27\Scripts And you are done! To check that it worked, open the Command Prompt and execute ``easy_install``. If you have User Account Control enabled on Windows Vista or Windows 7, it should prompt you for administrator privileges. +Now that you have ``easy_install``, you can use it to install ``pip``:: -.. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py + > easy_install pip + + +.. _distribute_setup.py: http://python-distribute.org/distribute_setup.py From 73a859533525febe42c0645ec25eeaa049123973 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 6 Feb 2012 20:41:13 -0500 Subject: [PATCH 0949/3747] vent and no more . --- docs/installation.rst | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0d3fcd74..075f12cb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -66,22 +66,18 @@ folder within:: $ mkdir myproject $ cd myproject - $ virtualenv env - New python executable in env/bin/python + $ virtualenv venv + New python executable in venv/bin/python Installing setuptools............done. Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: - $ . env/bin/activate - -(Note the space between the dot and the script name. The dot means that this -script should run in the context of the current shell. If this command does -not work in your shell, try replacing the dot with ``source``.) + $ source venv/bin/activate If you are a Windows user, the following command is for you:: - $ env\scripts\activate + $ venv\scripts\activate Either way, you should now be using your virtualenv (notice how the prompt of your shell has changed to show the active environment). @@ -89,7 +85,7 @@ your shell has changed to show the active environment). Now you can just enter the following command to get Flask activated in your virtualenv:: - $ easy_install Flask + $ pip install Flask A few seconds later and you are good to go. From 60f7e3a7b463db68f3cc22085b6c5eaac985e7f7 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 6 Feb 2012 20:41:13 -0500 Subject: [PATCH 0950/3747] vent and no more . --- docs/installation.rst | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0d3fcd74..71c4b59c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -66,22 +66,18 @@ folder within:: $ mkdir myproject $ cd myproject - $ virtualenv env - New python executable in env/bin/python + $ virtualenv venv + New python executable in venv/bin/python Installing setuptools............done. Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: - $ . env/bin/activate - -(Note the space between the dot and the script name. The dot means that this -script should run in the context of the current shell. If this command does -not work in your shell, try replacing the dot with ``source``.) + $ source venv/bin/activate If you are a Windows user, the following command is for you:: - $ env\scripts\activate + $ venv\scripts\activate Either way, you should now be using your virtualenv (notice how the prompt of your shell has changed to show the active environment). @@ -89,7 +85,7 @@ your shell has changed to show the active environment). Now you can just enter the following command to get Flask activated in your virtualenv:: - $ easy_install Flask + $ pip install Flask A few seconds later and you are good to go. @@ -118,10 +114,10 @@ Get the git checkout in a new virtualenv and run in development mode:: $ git clone http://github.com/mitsuhiko/flask.git Initialized empty Git repository in ~/dev/flask/.git/ $ cd flask - $ virtualenv env - $ . env/bin/activate + $ virtualenv venv --distribute New python executable in env/bin/python Installing setuptools............done. + $ source env/bin/activate $ python setup.py develop ... Finished processing dependencies for Flask @@ -134,10 +130,10 @@ To just get the development version without git, do this instead:: $ mkdir flask $ cd flask - $ virtualenv env - $ . env/bin/activate - New python executable in env/bin/python - Installing setuptools............done. + $ virtualenv venv --distribute + $ source venv/bin/activate + New python executable in venv/bin/python + Installing distribute............done. $ pip install Flask==dev ... Finished processing dependencies for Flask==dev From 5e848176e5b4db2958614e94024bd742830446eb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 6 Feb 2012 21:05:51 -0500 Subject: [PATCH 0951/3747] typos and fixes --- docs/installation.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 71c4b59c..a5fe6562 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -68,7 +68,7 @@ folder within:: $ cd myproject $ virtualenv venv New python executable in venv/bin/python - Installing setuptools............done. + Installing distribute............done. Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: @@ -99,7 +99,7 @@ This is possible as well, though I do not recommend it. Just run $ sudo pip install Flask (On Windows systems, run it in a command-prompt window with administrator -privleges, and leave out `sudo`.) +privileges, and leave out `sudo`.) Living on the Edge @@ -115,9 +115,9 @@ Get the git checkout in a new virtualenv and run in development mode:: Initialized empty Git repository in ~/dev/flask/.git/ $ cd flask $ virtualenv venv --distribute - New python executable in env/bin/python - Installing setuptools............done. - $ source env/bin/activate + New python executable in venv/bin/python + Installing distribute............done. + $ source venv/bin/activate $ python setup.py develop ... Finished processing dependencies for Flask From e070ede050fdb2ce155e13e29f5588c9831fa6b5 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 7 Feb 2012 10:42:25 -0500 Subject: [PATCH 0952/3747] Use "." not "source" for shell sourcing. Shell portability from mitsuhiko. --- docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index a5fe6562..8e6a4497 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -73,7 +73,7 @@ folder within:: Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: - $ source venv/bin/activate + $ . venv/bin/activate If you are a Windows user, the following command is for you:: @@ -117,7 +117,7 @@ Get the git checkout in a new virtualenv and run in development mode:: $ virtualenv venv --distribute New python executable in venv/bin/python Installing distribute............done. - $ source venv/bin/activate + $ . venv/bin/activate $ python setup.py develop ... Finished processing dependencies for Flask @@ -131,7 +131,7 @@ To just get the development version without git, do this instead:: $ mkdir flask $ cd flask $ virtualenv venv --distribute - $ source venv/bin/activate + $ . venv/bin/activate New python executable in venv/bin/python Installing distribute............done. $ pip install Flask==dev From 04bb720d3813c8cfe3ae46d2fb85dbc9a03a9b70 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 14 Feb 2012 18:13:29 -0500 Subject: [PATCH 0953/3747] Fix Blueprint example with template_folder, #403. --- docs/blueprints.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 9422fd02..4e3888c2 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -61,7 +61,8 @@ implement a blueprint that does simple rendering of static templates:: from flask import Blueprint, render_template, abort from jinja2 import TemplateNotFound - simple_page = Blueprint('simple_page', __name__) + simple_page = Blueprint('simple_page', __name__, + template_folder='templates') @simple_page.route('/', defaults={'page': 'index'}) @simple_page.route('/') From 0b3369355dadb39ac1ce9580d95004233031a287 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Fri, 24 Feb 2012 00:46:20 -0600 Subject: [PATCH 0954/3747] Allow loading template from iterable --- flask/templating.py | 8 +++++--- flask/testsuite/templating.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index 90e8772a..c809a63f 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -109,17 +109,19 @@ def _render(template, context, app): return rv -def render_template(template_name, **context): +def render_template(template_name_or_list, **context): """Renders a template from the template folder with the given context. - :param template_name: the name of the template to be rendered + :param template_name_or_list: the name of the template to be + rendered, or an iterable with template names + the first one existing will be rendered :param context: the variables that should be available in the context of the template. """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) - return _render(ctx.app.jinja_env.get_template(template_name), + return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list), context, ctx.app) diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 759fe0f3..4a0ebdbc 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -178,6 +178,25 @@ class TemplatingTestCase(FlaskTestCase): self.assert_equal(rv.data, 'Hello Custom World!') + def test_iterable_loader(self): + app = flask.Flask(__name__) + @app.context_processor + def context_processor(): + return {'whiskey': 'Jameson'} + @app.route('/') + def index(): + return flask.render_template( + ['no_template.xml', # should skip this one + 'simple_template.html', # should render this + 'context_template.html'], + value=23) + + rv = app.test_client().get('/') + self.assert_equal(rv.data, '

    Jameson

    ') + + + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TemplatingTestCase)) From fdad6713eba3f7aaae6ca4f6f50bb8b72452c6d9 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Fri, 24 Feb 2012 09:33:11 -0500 Subject: [PATCH 0955/3747] Add updates to render_template to CHANGES, #409. --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 7188e57b..8311e2e7 100644 --- a/CHANGES +++ b/CHANGES @@ -42,6 +42,9 @@ Relase date to be decided, codename to be chosen. using configuration values, e.g. ``app.run(app.config.get('MYHOST'), app.config.get('MYPORT'))``, with proper behavior whether or not a config file is provided. +- The :meth:`flask.render_template` method now accepts a either an iterable of + template names or a single template name. Previously, it only accepted a + single template name. On an iterable, the first template found is rendered. Version 0.8.1 From fc7fe628466857fd8b83c1b2b8501df2d15ae4a9 Mon Sep 17 00:00:00 2001 From: Andrew Ash Date: Wed, 22 Feb 2012 22:50:26 -0800 Subject: [PATCH 0956/3747] Update docs/errorhandling.rst --- docs/errorhandling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 97ff4df2..9e26196d 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -280,7 +280,7 @@ If you're using Aptana/Eclipse for debugging you'll need to set both ``use_debugger`` and ``use_reloader`` to False. A possible useful pattern for configuration is to set the following in your -config.yaml (change the block as approriate for your application, of course):: +config.yaml (change the block as appropriate for your application, of course):: FLASK: DEBUG: True From 20a3281209a5e99271c911f9c1143b1e6f0fb0b5 Mon Sep 17 00:00:00 2001 From: awsum Date: Tue, 21 Feb 2012 22:04:36 +0200 Subject: [PATCH 0957/3747] Update docs/patterns/wtforms.rst --- docs/patterns/wtforms.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index ed530427..1bf46637 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -85,8 +85,10 @@ Here's an example `_formhelpers.html` template with such a macro:
    {{ field.label }}
    {{ field(**kwargs)|safe }} {% if field.errors %} -
      - {% for error in field.errors %}
    • {{ error }}{% endfor %} +
        + {% for error in field.errors %} +
      • {{ error }}
      • + {% endfor %}
      {% endif %}
    @@ -106,7 +108,7 @@ takes advantage of the `_formhelpers.html` template: .. sourcecode:: html+jinja {% from "_formhelpers.html" import render_field %} -
    +
    {{ render_field(form.username) }} {{ render_field(form.email) }} From b9f4e0bd9c6e200ae930c2d9c07c582be118b051 Mon Sep 17 00:00:00 2001 From: Paul McMillan Date: Sun, 26 Feb 2012 12:43:50 -0800 Subject: [PATCH 0958/3747] Remove redundant words from quickstart. --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9fde0c2f..f23f957d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -168,8 +168,8 @@ The following converters exist: .. admonition:: Unique URLs / Redirection Behaviour - Flask's URL rules are based on Werkzeug's routing module. The idea behind - that module is to ensure beautiful and unique also unique URLs based on + Flask's URL rules are based on Werkzeug's routing module. The idea + behind that module is to ensure beautiful and unique URLs based on precedents laid down by Apache and earlier HTTP servers. Take these two rules:: @@ -234,7 +234,7 @@ some examples: (This also uses the :meth:`~flask.Flask.test_request_context` method, explained below. It tells Flask to behave as though it is handling a request, even -though were are interacting with it through a Python shell. Have a look at the +though we are interacting with it through a Python shell. Have a look at the explanation below. :ref:`context-locals`). Why would you want to build URLs instead of hard-coding them into your From 85ad4ffb605eee37cae9ffa1f82c5b6bb95c7820 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Thu, 1 Mar 2012 02:07:26 -0600 Subject: [PATCH 0959/3747] Blueprint example app --- examples/blueprintexample/blueprintexample.py | 11 ++++++++ .../blueprintexample/simple_page/__init__.py | 0 .../simple_page/simple_page.py | 13 ++++++++++ .../simple_page/templates/pages/hello.html | 5 ++++ .../simple_page/templates/pages/index.html | 5 ++++ .../simple_page/templates/pages/layout.html | 25 +++++++++++++++++++ .../simple_page/templates/pages/world.html | 5 ++++ 7 files changed, 64 insertions(+) create mode 100644 examples/blueprintexample/blueprintexample.py create mode 100644 examples/blueprintexample/simple_page/__init__.py create mode 100644 examples/blueprintexample/simple_page/simple_page.py create mode 100644 examples/blueprintexample/simple_page/templates/pages/hello.html create mode 100644 examples/blueprintexample/simple_page/templates/pages/index.html create mode 100644 examples/blueprintexample/simple_page/templates/pages/layout.html create mode 100644 examples/blueprintexample/simple_page/templates/pages/world.html diff --git a/examples/blueprintexample/blueprintexample.py b/examples/blueprintexample/blueprintexample.py new file mode 100644 index 00000000..bc0e41d4 --- /dev/null +++ b/examples/blueprintexample/blueprintexample.py @@ -0,0 +1,11 @@ +from flask import Flask +from simple_page.simple_page import simple_page + +app = Flask(__name__) +app.register_blueprint(simple_page) +# Blueprint can be registered many times +app.register_blueprint(simple_page, url_prefix='/pages') + + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/examples/blueprintexample/simple_page/__init__.py b/examples/blueprintexample/simple_page/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/blueprintexample/simple_page/simple_page.py b/examples/blueprintexample/simple_page/simple_page.py new file mode 100644 index 00000000..cb82cc37 --- /dev/null +++ b/examples/blueprintexample/simple_page/simple_page.py @@ -0,0 +1,13 @@ +from flask import Blueprint, render_template, abort +from jinja2 import TemplateNotFound + +simple_page = Blueprint('simple_page', __name__, + template_folder='templates') + +@simple_page.route('/', defaults={'page': 'index'}) +@simple_page.route('/') +def show(page): + try: + return render_template('pages/%s.html' % page) + except TemplateNotFound: + abort(404) diff --git a/examples/blueprintexample/simple_page/templates/pages/hello.html b/examples/blueprintexample/simple_page/templates/pages/hello.html new file mode 100644 index 00000000..7fca6668 --- /dev/null +++ b/examples/blueprintexample/simple_page/templates/pages/hello.html @@ -0,0 +1,5 @@ +{% extends "pages/layout.html" %} + +{% block body %} + Hello +{% endblock %} \ No newline at end of file diff --git a/examples/blueprintexample/simple_page/templates/pages/index.html b/examples/blueprintexample/simple_page/templates/pages/index.html new file mode 100644 index 00000000..0ca3ffe2 --- /dev/null +++ b/examples/blueprintexample/simple_page/templates/pages/index.html @@ -0,0 +1,5 @@ +{% extends "pages/layout.html" %} + +{% block body %} + Blueprint example page +{% endblock %} \ No newline at end of file diff --git a/examples/blueprintexample/simple_page/templates/pages/layout.html b/examples/blueprintexample/simple_page/templates/pages/layout.html new file mode 100644 index 00000000..2efccb95 --- /dev/null +++ b/examples/blueprintexample/simple_page/templates/pages/layout.html @@ -0,0 +1,25 @@ + +Simple Page Blueprint +
    +

    This is blueprint example

    +

    + A simple page blueprint is registered under / and /pages
    + you can access it using this urls: +

    +

    +

    + Also you can register the same blueprint under another path +

    +

    + + + + {% block body %} + {% endblock %} +
    \ No newline at end of file diff --git a/examples/blueprintexample/simple_page/templates/pages/world.html b/examples/blueprintexample/simple_page/templates/pages/world.html new file mode 100644 index 00000000..bdb5b16b --- /dev/null +++ b/examples/blueprintexample/simple_page/templates/pages/world.html @@ -0,0 +1,5 @@ +{% extends "pages/layout.html" %} + +{% block body %} + World +{% endblock %} \ No newline at end of file From 62621ccd133ffcbe2c88d18841c0669d03739ac0 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Thu, 1 Mar 2012 02:24:56 -0600 Subject: [PATCH 0960/3747] Blueprint example tests --- .../blueprintexample/blueprintexample_test.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/blueprintexample/blueprintexample_test.py diff --git a/examples/blueprintexample/blueprintexample_test.py b/examples/blueprintexample/blueprintexample_test.py new file mode 100644 index 00000000..b8f93414 --- /dev/null +++ b/examples/blueprintexample/blueprintexample_test.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" + Blueprint Example Tests + ~~~~~~~~~~~~~~ + + Tests the Blueprint example app +""" +import blueprintexample +import unittest + + +class BlueprintExampleTestCase(unittest.TestCase): + + def setUp(self): + self.app = blueprintexample.app.test_client() + + def test_urls(self): + r = self.app.get('/') + self.assertEquals(r.status_code, 200) + + r = self.app.get('/hello') + self.assertEquals(r.status_code, 200) + + r = self.app.get('/world') + self.assertEquals(r.status_code, 200) + + #second blueprint instance + r = self.app.get('/pages/hello') + self.assertEquals(r.status_code, 200) + + r = self.app.get('/pages/world') + self.assertEquals(r.status_code, 200) + + +if __name__ == '__main__': + unittest.main() From 76773e1d0a8cabbe048bb76f9306185e9d83a85c Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Thu, 1 Mar 2012 08:34:08 -0500 Subject: [PATCH 0961/3747] Fixed silent keyword arg to config.from_envvar. The ``silent`` keyword argument to Config.from_envvar was not being honored if the environment variable existed but the file that it mentioned did not. The fix was simple - pass the keyword argument on to the underlying call to ``from_pyfile``. I also noticed that the return value from ``from_pyfile`` was not being passed back so I fixed that as well. --- flask/config.py | 3 +-- flask/testsuite/config.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/flask/config.py b/flask/config.py index 67dbf9b7..759fd488 100644 --- a/flask/config.py +++ b/flask/config.py @@ -106,8 +106,7 @@ class Config(dict): 'loaded. Set this variable and make it ' 'point to a configuration file' % variable_name) - self.from_pyfile(rv) - return True + return self.from_pyfile(rv, silent=silent) def from_pyfile(self, filename, silent=False): """Updates the values in the config from a Python file. This function diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 00f77cea..e10804c3 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -69,6 +69,24 @@ class ConfigTestCase(FlaskTestCase): finally: os.environ = env + def test_config_from_envvar_missing(self): + env = os.environ + try: + os.environ = {'FOO_SETTINGS': 'missing.cfg'} + try: + app = flask.Flask(__name__) + app.config.from_envvar('FOO_SETTINGS') + except IOError, e: + msg = str(e) + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) + else: + self.assert_(0, 'expected config') + self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + finally: + os.environ = env + def test_config_missing(self): app = flask.Flask(__name__) try: From 8d7ca29a3554d20324ed9c75d9c095dfa8a8c439 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Thu, 1 Mar 2012 08:53:58 -0500 Subject: [PATCH 0962/3747] Cleaned up test case for issue #414. --- flask/testsuite/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index e10804c3..bf72925b 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -82,8 +82,8 @@ class ConfigTestCase(FlaskTestCase): 'file (No such file or directory):')) self.assert_(msg.endswith("missing.cfg'")) else: - self.assert_(0, 'expected config') - self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + self.fail('expected IOError') + self.assertFalse(app.config.from_envvar('FOO_SETTINGS', silent=True)) finally: os.environ = env From 8445f0d939dc3c4a2e722dc6dd4938d02bc2e094 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Fri, 2 Mar 2012 07:46:39 -0300 Subject: [PATCH 0963/3747] Fixed assumption made on session implementations. In the snippet 'session.setdefault(...).append(...)', it was being assumed that changes made to mutable structures in the session are are always in sync with the session object, which is not true for session implementations that use a external storage for keeping their keys/values. --- flask/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 25250d26..122c330f 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -261,7 +261,9 @@ def flash(message, category='message'): messages and ``'warning'`` for warnings. However any kind of string can be used as category. """ - session.setdefault('_flashes', []).append((category, message)) + flashes = session.get('_flashes', []) + flashes.append((category, message)) + session['_flashes'] = flashes def get_flashed_messages(with_categories=False, category_filter=[]): From 7ed3cba6588fbd585b10e58e15e352e90874732d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 8 Mar 2012 09:14:14 -0800 Subject: [PATCH 0964/3747] Split ebook build process into own make target. --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6fa48693..08811ac2 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,6 @@ clean-pyc: find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + -# ebook-convert docs: http://manual.calibre-ebook.com/cli/ebook-convert.html upload-docs: $(MAKE) -C docs html dirhtml latex epub $(MAKE) -C docs/_build/latex all-pdf @@ -31,7 +30,10 @@ upload-docs: rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip rsync -a docs/_build/epub/Flask.epub pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.epub - @echo 'Building .mobi from .epub...' + +# ebook-convert docs: http://manual.calibre-ebook.com/cli/ebook-convert.html +ebook: + @echo 'Using .epub from `make upload-docs` to create .mobi.' @echo 'Command `ebook-covert` is provided by calibre package.' @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' @echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' From 9711fd402010b2e14a98879f0457174c3ca15a24 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Thu, 8 Mar 2012 16:41:39 -0500 Subject: [PATCH 0965/3747] On JSON requests, the JSON response should have Content-Type: application/json and the body of the response should be a JSON object. --- CHANGES | 2 ++ flask/testsuite/helpers.py | 12 ++++++++ flask/wrappers.py | 56 +++++++++++++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 8311e2e7..91a31040 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Version 0.9 Relase date to be decided, codename to be chosen. +- The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted + response by default. - The :func:`flask.url_for` function now can generate anchors to the generated links. - The :func:`flask.url_for` function now can also explicitly generate diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 89c6c188..e48f8dc3 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -40,6 +40,18 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) + def test_json_bad_requests_content_type(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_('description' in flask.json.loads(rv.data)) + self.assert_('

    ' not in flask.json.loads(rv.data)['description']) + def test_json_body_encoding(self): app = flask.Flask(__name__) app.testing = True diff --git a/flask/wrappers.py b/flask/wrappers.py index f6ec2788..3df697f7 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -10,7 +10,7 @@ """ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase -from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequest, HTTPException from werkzeug.utils import cached_property from .debughelpers import attach_enctype_error_multidict @@ -18,6 +18,43 @@ from .helpers import json, _assert_have_json from .globals import _request_ctx_stack +class JSONHTTPException(HTTPException): + """A base class for HTTP exceptions with ``Content-Type: + application/json``. + + The ``description`` attribute of this class must set to a string (*not* an + HTML string) which describes the error. + + """ + + def get_body(self, environ): + """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to + return the description of this error in JSON format instead of HTML. + + """ + return json.dumps(dict(description=self.get_description(environ))) + + def get_headers(self, environ): + """Returns a list of headers including ``Content-Type: + application/json``. + + """ + return [('Content-Type', 'application/json')] + + +class JSONBadRequest(JSONHTTPException, BadRequest): + """Represents an HTTP ``400 Bad Request`` error whose body contains an + error message in JSON format instead of HTML format (as in the superclass). + + """ + + #: The description of the error which occurred as a string. + description = ( + 'The browser (or proxy) sent a request that this server could not ' + 'understand.' + ) + + class Request(RequestBase): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. @@ -108,12 +145,23 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of - this method is used by :attr:`json` when an error ocurred. The - default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + this method is used by :attr:`json` when an error ocurred. The default + implementation raises a :class:`JSONBadRequest`, which is a subclass of + :class:`~werkzeug.exceptions.BadRequest` which sets the + ``Content-Type`` to ``application/json`` and provides a JSON-formatted + error description:: + + {"description": "The browser (or proxy) sent a request that \ + this server could not understand."} + + .. versionchanged:: 0.9 + + Return a :class:`JSONBadRequest` instead of a + :class:`~werkzeug.exceptions.BadRequest` by default. .. versionadded:: 0.8 """ - raise BadRequest() + raise JSONBadRequest() def _load_form_data(self): RequestBase._load_form_data(self) From 6b9e6a5a52f22bdf6b86b76de1fc7c8e1a635a8f Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 11 Mar 2012 20:20:32 -0700 Subject: [PATCH 0966/3747] add heroku/deploy options to quickstart, and add more clear links in tutorial setup. --- docs/quickstart.rst | 36 ++++++++++++++++++++++++++++++++++++ docs/tutorial/setup.rst | 20 ++++++++++---------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9fde0c2f..ed11316c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -826,3 +826,39 @@ can do it like this:: from werkzeug.contrib.fixers import LighttpdCGIRootFix app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) + +Share your Local Server with a Friend +------------------------------------- + +`Localtunnel `_ is a neat tool you can use to +quickly share your local Flask server to a friend. + +To install Localtunnel, open a terminal and run the following command:: + + sudo gem install localtunnel + +Then, with Flask running at ``http://localhost:5000``, open a new Terminal window +and type:: + + localtunnel 5000 + Port 5000 is now publicly accessible from http://54xy.localtunnel.com ... + +*(Get a* ``gem: command not found`` *error? Download RubyGems* +`here `_ *.)* + +If you load the URL given in the localtunnel output in your browser, you +should see your Flask app. It's actually being loaded from your own computer! + +Deploying to a Web Server +------------------------- + +`Heroku `_ offers a free web platform to host your +Flask app, and is the easiest way for you to put your Flask app online. +They have excellent instructions on how to deploy your Flask app `here +`_. + +Other resources for deploying Flask apps: + +- `Deploying Flask on ep.io `_ +- `Deploying Flask on Webfaction `_ +- `Deploying Flask on Google App Engine `_ diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index e9e4d679..3a8fba33 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -11,7 +11,7 @@ into the module which we will be doing here. However a cleaner solution would be to create a separate `.ini` or `.py` file and load that or import the values from there. -:: +In `flaskr.py`:: # all the imports import sqlite3 @@ -26,7 +26,7 @@ the values from there. PASSWORD = 'default' Next we can create our actual application and initialize it with the -config from the same file:: +config from the same file, in `flaskr.py`:: # create our little application :) app = Flask(__name__) @@ -37,21 +37,21 @@ string it will import it) and then look for all uppercase variables defined there. In our case, the configuration we just wrote a few lines of code above. You can also move that into a separate file. -It is also a good idea to be able to load a configuration from a -configurable file. This is what :meth:`~flask.Config.from_envvar` can -do:: +Usually, it is a good idea to load a configuration from a configurable +file. This is what :meth:`~flask.Config.from_envvar` can do, replacing the +:meth:`~flask.Config.from_object` line above:: app.config.from_envvar('FLASKR_SETTINGS', silent=True) That way someone can set an environment variable called -:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will -then override the default values. The silent switch just tells Flask to -not complain if no such environment key is set. +:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will then +override the default values. The silent switch just tells Flask to not complain +if no such environment key is set. The `secret_key` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The -debug flag enables or disables the interactive debugger. Never leave -debug mode activated in a production system because it will allow users to +debug flag enables or disables the interactive debugger. *Never leave +debug mode activated in a production system*, because it will allow users to execute code on the server! We also add a method to easily connect to the database specified. That From 605d0ee34421fbc1dd714790ff016834c9b3f0cb Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 11 Mar 2012 20:33:43 -0700 Subject: [PATCH 0967/3747] update links --- docs/quickstart.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ed11316c..f1771503 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -852,12 +852,13 @@ should see your Flask app. It's actually being loaded from your own computer! Deploying to a Web Server ------------------------- -`Heroku `_ offers a free web platform to host your -Flask app, and is the easiest way for you to put your Flask app online. -They have excellent instructions on how to deploy your Flask app `here -`_. +If you want to make your Flask app available to the Internet at large, `Heroku +`_ is very easy to set up and will run small Flask +applications for free. `Check out their tutorial on how to deploy Flask apps on +their service `_. -Other resources for deploying Flask apps: +There are a number of other websites that will host your Flask app and make it +easy for you to do so. - `Deploying Flask on ep.io `_ - `Deploying Flask on Webfaction `_ From 075b6b11c8b1690d946b8839e6dc4eb8a8cb7e3c Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Sun, 11 Mar 2012 20:45:58 -0700 Subject: [PATCH 0968/3747] Fix issue 140 This allows for a view function to return something like: jsonify(error="error msg"), 400 --- CHANGES | 3 +++ flask/app.py | 16 +++++++++++++++- flask/testsuite/basic.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 8311e2e7..dbf447db 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,9 @@ Relase date to be decided, codename to be chosen. - The :meth:`flask.render_template` method now accepts a either an iterable of template names or a single template name. Previously, it only accepted a single template name. On an iterable, the first template found is rendered. +- View functions can now return a tuple with the first instance being an + instance of :class:`flask.Response`. This allows for returning + ``jsonify(error="error msg"), 400`` from a view function. Version 0.8.1 diff --git a/flask/app.py b/flask/app.py index 15e432de..f3d7efcb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1361,7 +1361,21 @@ class Flask(_PackageBoundObject): if isinstance(rv, basestring): return self.response_class(rv) if isinstance(rv, tuple): - return self.response_class(*rv) + if len(rv) > 0 and isinstance(rv[0], self.response_class): + original = rv[0] + new_response = self.response_class('', *rv[1:]) + if len(rv) < 3: + # The args for the response class are + # response=None, status=None, headers=None, + # mimetype=None, content_type=None, ... + # so if there's at least 3 elements the rv + # tuple contains header information so the + # headers from rv[0] "win." + new_response.headers = original.headers + new_response.response = original.response + return new_response + else: + return self.response_class(*rv) return self.response_class.force_type(rv, request.environ) def create_url_adapter(self, request): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index e6a278e5..41efb196 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -659,6 +659,35 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(rv.data, 'W00t') self.assert_equal(rv.mimetype, 'text/html') + def test_make_response_with_response_instance(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response( + flask.jsonify({'msg': 'W00t'}), 400) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.data, + '{\n "msg": "W00t"\n}') + self.assertEqual(rv.mimetype, 'application/json') + + rv = flask.make_response( + flask.Response(''), 400) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.data, '') + self.assertEqual(rv.mimetype, 'text/html') + + rv = flask.make_response( + flask.Response('', headers={'Content-Type': 'text/html'}), + 400, None, 'application/json') + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.headers['Content-Type'], 'application/json') + + rv = flask.make_response( + flask.Response('', mimetype='application/json'), + 400, {'Content-Type': 'text/html'}) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.headers['Content-Type'], 'text/html') + + def test_url_generation(self): app = flask.Flask(__name__) @app.route('/hello/', methods=['POST']) From 2befab24c50a8f1a3417a7ce22e65608ba3905e1 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 11 Mar 2012 23:17:40 -0700 Subject: [PATCH 0969/3747] remove localtunnel things that were added to snippets --- docs/quickstart.rst | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f1771503..de62e546 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -827,28 +827,6 @@ can do it like this:: from werkzeug.contrib.fixers import LighttpdCGIRootFix app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) -Share your Local Server with a Friend -------------------------------------- - -`Localtunnel `_ is a neat tool you can use to -quickly share your local Flask server to a friend. - -To install Localtunnel, open a terminal and run the following command:: - - sudo gem install localtunnel - -Then, with Flask running at ``http://localhost:5000``, open a new Terminal window -and type:: - - localtunnel 5000 - Port 5000 is now publicly accessible from http://54xy.localtunnel.com ... - -*(Get a* ``gem: command not found`` *error? Download RubyGems* -`here `_ *.)* - -If you load the URL given in the localtunnel output in your browser, you -should see your Flask app. It's actually being loaded from your own computer! - Deploying to a Web Server ------------------------- @@ -863,3 +841,4 @@ easy for you to do so. - `Deploying Flask on ep.io `_ - `Deploying Flask on Webfaction `_ - `Deploying Flask on Google App Engine `_ +- `Sharing your Localhost Server with Localtunnel `_ From 06b224676d2f6c38fbf1f486f636e81a85016d45 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Mon, 12 Mar 2012 11:19:17 -0400 Subject: [PATCH 0970/3747] Added _PackageBoundObject.get_static_file_options. This method receives the name of a static file that is going to be served up and generates a dict of options to use when serving the file. The default set is empty so code will fall back to the existing behavior if the method is not overridden. I needed this method to adjust the cache control headers for .js files that one of my applications was statically serving. The default expiration is buried in an argument to send_file and is set to 12 hours. There was no good way to adjust this value previously. --- flask/helpers.py | 11 +++++++++-- flask/testsuite/helpers.py | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 25250d26..4cb918d2 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -495,7 +495,8 @@ def send_from_directory(directory, filename, **options): filename = safe_join(directory, filename) if not os.path.isfile(filename): raise NotFound() - return send_file(filename, conditional=True, **options) + options.setdefault('conditional', True) + return send_file(filename, **options) def get_root_path(import_name): @@ -651,6 +652,11 @@ class _PackageBoundObject(object): return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + def get_static_file_options(self, filename): + """Function used internally to determine what keyword arguments + to send to :func:`send_from_directory` for a specific file.""" + return {} + def send_static_file(self, filename): """Function used internally to send static files from the static folder to the browser. @@ -659,7 +665,8 @@ class _PackageBoundObject(object): """ if not self.has_static_folder: raise RuntimeError('No static folder for this object') - return send_from_directory(self.static_folder, filename) + return send_from_directory(self.static_folder, filename, + **self.get_static_file_options(filename)) def open_resource(self, resource, mode='rb'): """Opens a resource from the application's resource folder. To see diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 89c6c188..c88026d9 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -17,7 +17,7 @@ import unittest from logging import StreamHandler from StringIO import StringIO from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr -from werkzeug.http import parse_options_header +from werkzeug.http import parse_cache_control_header, parse_options_header def has_encoding(name): @@ -204,6 +204,30 @@ class SendfileTestCase(FlaskTestCase): self.assert_equal(value, 'attachment') self.assert_equal(options['filename'], 'index.txt') + def test_static_file(self): + app = flask.Flask(__name__) + # default cache timeout is 12 hours (hard-coded) + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 12 * 60 * 60) + # override get_static_file_options with some new values and check them + class StaticFileApp(flask.Flask): + def __init__(self): + super(StaticFileApp, self).__init__(__name__) + def get_static_file_options(self, filename): + opts = super(StaticFileApp, self).get_static_file_options(filename) + opts['cache_timeout'] = 10 + # this test catches explicit inclusion of the conditional + # keyword arg in the guts + opts['conditional'] = True + return opts + app = StaticFileApp() + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 10) + class LoggingTestCase(FlaskTestCase): From 2d237f3c533baa768b29d808f1850382542bbd2f Mon Sep 17 00:00:00 2001 From: Matt Dawson Date: Mon, 12 Mar 2012 10:57:00 -0700 Subject: [PATCH 0971/3747] Fix grammar in extension dev docs. --- docs/extensiondev.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 074d06ab..5a8d5b16 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -332,9 +332,9 @@ extension to be approved you have to follow these guidelines: 2. It must ship a testing suite that can either be invoked with ``make test`` or ``python setup.py test``. For test suites invoked with ``make test`` the extension has to ensure that all dependencies for the test - are installed automatically, in case of ``python setup.py test`` - dependencies for tests alone can be specified in the `setup.py` - file. The test suite also has to be part of the distribution. + are installed automatically. If tests are invoked with ``python setup.py + test``, test dependencies can be specified in the `setup.py` file. The + test suite also has to be part of the distribution. 3. APIs of approved extensions will be checked for the following characteristics: From 8216e036e96594a4932d1f6a3569fcf39fe3c2bd Mon Sep 17 00:00:00 2001 From: Thibaud Morel Date: Mon, 12 Mar 2012 11:16:03 -0700 Subject: [PATCH 0972/3747] Specifying supported Python versions in setup.py metadata --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 3a302cea..812b2c8a 100644 --- a/setup.py +++ b/setup.py @@ -100,6 +100,9 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], From cb24646948a8c00c5b39f0d76bf75e153ac502b1 Mon Sep 17 00:00:00 2001 From: Christoph Heer Date: Tue, 17 Jan 2012 21:09:59 +0100 Subject: [PATCH 0973/3747] Add jsonp support inside of jsonify --- flask/helpers.py | 21 +++++++++++++++++++++ flask/testsuite/helpers.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index 25250d26..ebe5fca5 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -117,9 +117,30 @@ def jsonify(*args, **kwargs): information about this, have a look at :ref:`json-security`. .. versionadded:: 0.2 + + .. versionadded:: 0.9 + If the argument ``padded`` true than the json object will pad for + JSONP calls like from jquery. The response mimetype will also change + to ``text/javascript``. + + The json object will pad as javascript function with the function name + from the request argument ``callback`` or ``jsonp``. If the argument + ``padded`` a string jsonify will look for the function name in the + request argument with the name which is equal to ``padded``. Is there + no function name it will fallback and use ``jsonp`` as function name. """ if __debug__: _assert_have_json() + if 'padded' in kwargs: + if isinstance(kwargs['padded'], str): + callback = request.args.get(kwargs['padded']) or 'jsonp' + else: + callback = request.args.get('callback') or \ + request.args.get('jsonp') or 'jsonp' + del kwargs['padded'] + json_str = json.dumps(dict(*args, **kwargs), indent=None) + content = str(callback) + "(" + json_str + ")" + return current_app.response_class(content, mimetype='text/javascript') return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 89c6c188..44ac9016 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -61,11 +61,25 @@ class JSONTestCase(FlaskTestCase): @app.route('/dict') def return_dict(): return flask.jsonify(d) + @app.route("/padded") + def return_padded_json(): + return flask.jsonify(d, padded=True) + @app.route("/padded_custom") + def return_padded_json_custom_callback(): + return flask.jsonify(d, padded='my_func_name') c = app.test_client() for url in '/kw', '/dict': rv = c.get(url) self.assert_equal(rv.mimetype, 'application/json') self.assert_equal(flask.json.loads(rv.data), d) + for get_arg in 'callback=funcName', 'jsonp=funcName': + rv = c.get('/padded?' + get_arg) + self.assert_( rv.data.startswith("funcName(") ) + self.assert_( rv.data.endswith(")") ) + rv_json = rv.data.split('(')[1].split(')')[0] + self.assert_equal(flask.json.loads(rv_json), d) + rv = c.get('/padded_custom?my_func_name=funcName') + self.assert_( rv.data.startswith("funcName(") ) def test_json_attr(self): app = flask.Flask(__name__) From 09370c3f1c808e9251292bc228b6bef4b1223e93 Mon Sep 17 00:00:00 2001 From: Ned Jackson Lovely Date: Mon, 12 Mar 2012 15:26:05 -0400 Subject: [PATCH 0974/3747] Clean up docs and review pull request #384 Spelunking through the issues at the PyCon sprints. --- flask/helpers.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index ebe5fca5..ee68ce95 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -119,15 +119,16 @@ def jsonify(*args, **kwargs): .. versionadded:: 0.2 .. versionadded:: 0.9 - If the argument ``padded`` true than the json object will pad for - JSONP calls like from jquery. The response mimetype will also change - to ``text/javascript``. + If the ``padded`` argument is true, the JSON object will be padded + for JSONP calls and the response mimetype will be changed to + ``text/javascript``. By default, the request arguments ``callback`` + and ``jsonp`` will be used as the name for the callback function. + This will work with jQuery and most other JavaScript libraries + by default. - The json object will pad as javascript function with the function name - from the request argument ``callback`` or ``jsonp``. If the argument - ``padded`` a string jsonify will look for the function name in the - request argument with the name which is equal to ``padded``. Is there - no function name it will fallback and use ``jsonp`` as function name. + If the ``padded`` argument is a string, jsonify will look for + the request argument with the same name and use that value as the + callback-function name. """ if __debug__: _assert_have_json() From 27194a01d8f7e4fd913811859ec4051ff99c52f4 Mon Sep 17 00:00:00 2001 From: Ned Jackson Lovely Date: Mon, 12 Mar 2012 16:02:53 -0400 Subject: [PATCH 0975/3747] Fix typo in docs. http://feedback.flask.pocoo.org/message/279 --- docs/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9fde0c2f..b442af28 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -169,8 +169,8 @@ The following converters exist: .. admonition:: Unique URLs / Redirection Behaviour Flask's URL rules are based on Werkzeug's routing module. The idea behind - that module is to ensure beautiful and unique also unique URLs based on - precedents laid down by Apache and earlier HTTP servers. + that module is to ensure beautiful and unique URLs based on precedents + laid down by Apache and earlier HTTP servers. Take these two rules:: From 68f93634de2e25afda209b710002e4c9159fd38e Mon Sep 17 00:00:00 2001 From: Ned Jackson Lovely Date: Mon, 12 Mar 2012 17:18:27 -0400 Subject: [PATCH 0976/3747] Second thoughts on mime type After further review, changing the mime type on jsonp responses from text/javascript to application/javascript, with a hat-tip to http://stackoverflow.com/questions/111302/best-content-type-to-serve-jsonp --- flask/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index ee68ce95..31a0f693 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -121,7 +121,7 @@ def jsonify(*args, **kwargs): .. versionadded:: 0.9 If the ``padded`` argument is true, the JSON object will be padded for JSONP calls and the response mimetype will be changed to - ``text/javascript``. By default, the request arguments ``callback`` + ``application/javascript``. By default, the request arguments ``callback`` and ``jsonp`` will be used as the name for the callback function. This will work with jQuery and most other JavaScript libraries by default. @@ -141,7 +141,7 @@ def jsonify(*args, **kwargs): del kwargs['padded'] json_str = json.dumps(dict(*args, **kwargs), indent=None) content = str(callback) + "(" + json_str + ")" - return current_app.response_class(content, mimetype='text/javascript') + return current_app.response_class(content, mimetype='application/javascript') return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') From 71b351173b1440d6ca2dc36284d080fda2a22006 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 12 Mar 2012 14:53:24 -0700 Subject: [PATCH 0977/3747] Move JSONHTTPException and JSONBadRequest to new module flask.exceptions. --- flask/exceptions.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ flask/wrappers.py | 39 +----------------------------------- 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 flask/exceptions.py diff --git a/flask/exceptions.py b/flask/exceptions.py new file mode 100644 index 00000000..9ccdedab --- /dev/null +++ b/flask/exceptions.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" + flask.exceptions + ~~~~~~~~~~~~ + + Flask specific additions to :class:`~werkzeug.exceptions.HTTPException` + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from werkzeug.exceptions import HTTPException, BadRequest +from .helpers import json + + +class JSONHTTPException(HTTPException): + """A base class for HTTP exceptions with ``Content-Type: + application/json``. + + The ``description`` attribute of this class must set to a string (*not* an + HTML string) which describes the error. + + """ + + def get_body(self, environ): + """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to + return the description of this error in JSON format instead of HTML. + + """ + return json.dumps(dict(description=self.get_description(environ))) + + def get_headers(self, environ): + """Returns a list of headers including ``Content-Type: + application/json``. + + """ + return [('Content-Type', 'application/json')] + + +class JSONBadRequest(JSONHTTPException, BadRequest): + """Represents an HTTP ``400 Bad Request`` error whose body contains an + error message in JSON format instead of HTML format (as in the superclass). + + """ + + #: The description of the error which occurred as a string. + description = ( + 'The browser (or proxy) sent a request that this server could not ' + 'understand.' + ) diff --git a/flask/wrappers.py b/flask/wrappers.py index 3df697f7..541d26ef 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -10,51 +10,14 @@ """ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase -from werkzeug.exceptions import BadRequest, HTTPException from werkzeug.utils import cached_property +from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict from .helpers import json, _assert_have_json from .globals import _request_ctx_stack -class JSONHTTPException(HTTPException): - """A base class for HTTP exceptions with ``Content-Type: - application/json``. - - The ``description`` attribute of this class must set to a string (*not* an - HTML string) which describes the error. - - """ - - def get_body(self, environ): - """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to - return the description of this error in JSON format instead of HTML. - - """ - return json.dumps(dict(description=self.get_description(environ))) - - def get_headers(self, environ): - """Returns a list of headers including ``Content-Type: - application/json``. - - """ - return [('Content-Type', 'application/json')] - - -class JSONBadRequest(JSONHTTPException, BadRequest): - """Represents an HTTP ``400 Bad Request`` error whose body contains an - error message in JSON format instead of HTML format (as in the superclass). - - """ - - #: The description of the error which occurred as a string. - description = ( - 'The browser (or proxy) sent a request that this server could not ' - 'understand.' - ) - - class Request(RequestBase): """The request object used by default in Flask. Remembers the matched endpoint and view arguments. From 74a72e86addd80a060f1abf9fe51bfc3f5d5be8b Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 12 Mar 2012 14:58:26 -0700 Subject: [PATCH 0978/3747] Changed some things in the foreward to diminish its discouragement. --- docs/foreword.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 7678d014..539f2897 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -8,7 +8,7 @@ should or should not be using it. What does "micro" mean? ----------------------- -To me, the "micro" in microframework refers not only to the simplicity and +As Flask considers it, the "micro" in microframework refers not only to the simplicity and small size of the framework, but also the fact that it does not make many decisions for you. While Flask does pick a templating engine for you, we won't make such decisions for your datastore or other parts. @@ -55,7 +55,7 @@ section about :ref:`design`. Web Development is Dangerous ---------------------------- -I'm not joking. Well, maybe a little. If you write a web +If you write a web application, you are probably allowing users to register and leave their data on your server. The users are entrusting you with data. And even if you are the only user that might leave data in your application, you still From d8c2ec4cd863112af4c55e1044c8d3024d58f21a Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 12 Mar 2012 15:03:26 -0700 Subject: [PATCH 0979/3747] Fixed linebreaks. --- docs/foreword.rst | 150 ++++++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 539f2897..1fa214e6 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -1,100 +1,104 @@ -Foreword +Foreword ======== -Read this before you get started with Flask. This hopefully answers some -questions about the purpose and goals of the project, and when you -should or should not be using it. +Read this before you get started with Flask. This hopefully answers +some questions about the purpose and goals of the project, and when +you should or should not be using it. -What does "micro" mean? +What does "micro" mean? ----------------------- -As Flask considers it, the "micro" in microframework refers not only to the simplicity and -small size of the framework, but also the fact that it does not make many -decisions for you. While Flask does pick a templating engine for you, we -won't make such decisions for your datastore or other parts. +As Flask considers it, the "micro" in microframework refers not only +to the simplicity and small size of the framework, but also the fact +that it does not make many decisions for you. While Flask does pick a +templating engine for you, we won't make such decisions for your +datastore or other parts. -However, to us the term “micro” does not mean that the whole implementation -has to fit into a single Python file. +However, to us the term “micro” does not mean that the whole +implementation has to fit into a single Python file. One of the design decisions with Flask was that simple tasks should be -simple; they should not take a lot of code and yet they should not limit you. -Because of that we made a few design choices that some people might find -surprising or unorthodox. For example, Flask uses thread-local objects -internally so that you don't have to pass objects around from function to -function within a request in order to stay threadsafe. While this is a -really easy approach and saves you a lot of time, it might also cause some -troubles for very large applications because changes on these thread-local -objects can happen anywhere in the same thread. In order to solve these -problems we don't hide the thread locals for you but instead embrace them -and provide you with a lot of tools to make it as pleasant as possible to -work with them. +simple; they should not take a lot of code and yet they should not +limit you. Because of that we made a few design choices that some +people might find surprising or unorthodox. For example, Flask uses +thread-local objects internally so that you don't have to pass objects +around from function to function within a request in order to stay +threadsafe. While this is a really easy approach and saves you a lot +of time, it might also cause some troubles for very large applications +because changes on these thread-local objects can happen anywhere in +the same thread. In order to solve these problems we don't hide the +thread locals for you but instead embrace them and provide you with a +lot of tools to make it as pleasant as possible to work with them. Flask is also based on convention over configuration, which means that -many things are preconfigured. For example, by convention templates and -static files are stored in subdirectories within the application's Python source tree. -While this can be changed you usually don't have to. +many things are preconfigured. For example, by convention templates +and static files are stored in subdirectories within the application's +Python source tree. While this can be changed you usually don't have +to. -The main reason Flask is called a "microframework" is the idea -to keep the core simple but extensible. There is no database abstraction +The main reason Flask is called a "microframework" is the idea to keep +the core simple but extensible. There is no database abstraction layer, no form validation or anything else where different libraries -already exist that can handle that. However Flask supports -extensions to add such functionality to your application as if it -was implemented in Flask itself. There are currently extensions for -object-relational mappers, form validation, upload handling, various open -authentication technologies and more. +already exist that can handle that. However Flask supports extensions +to add such functionality to your application as if it was implemented +in Flask itself. There are currently extensions for object-relational +mappers, form validation, upload handling, various open authentication +technologies and more. -Since Flask is based on a very solid foundation there is not a lot of code -in Flask itself. As such it's easy to adapt even for large applications -and we are making sure that you can either configure it as much as -possible by subclassing things or by forking the entire codebase. If you -are interested in that, check out the :ref:`becomingbig` chapter. +Since Flask is based on a very solid foundation there is not a lot of +code in Flask itself. As such it's easy to adapt even for large +applications and we are making sure that you can either configure it +as much as possible by subclassing things or by forking the entire +codebase. If you are interested in that, check out the +:ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the section about :ref:`design`. -Web Development is Dangerous ----------------------------- +Web Development is Dangerous ---------------------------- -If you write a web -application, you are probably allowing users to register and leave their -data on your server. The users are entrusting you with data. And even if -you are the only user that might leave data in your application, you still -want that data to be stored securely. +If you write a web application, you are probably allowing users to +register and leave their data on your server. The users are +entrusting you with data. And even if you are the only user that +might leave data in your application, you still want that data to be +stored securely. -Unfortunately, there are many ways the security of a web application can be -compromised. Flask protects you against one of the most common security -problems of modern web applications: cross-site scripting (XSS). Unless -you deliberately mark insecure HTML as secure, Flask and the underlying -Jinja2 template engine have you covered. But there are many more ways to -cause security problems. +Unfortunately, there are many ways the security of a web application +can be compromised. Flask protects you against one of the most common +security problems of modern web applications: cross-site scripting +(XSS). Unless you deliberately mark insecure HTML as secure, Flask +and the underlying Jinja2 template engine have you covered. But there +are many more ways to cause security problems. The documentation will warn you about aspects of web development that -require attention to security. Some of these security concerns -are far more complex than one might think, and we all sometimes underestimate -the likelihood that a vulnerability will be exploited - until a clever -attacker figures out a way to exploit our applications. And don't think -that your application is not important enough to attract an attacker. -Depending on the kind of attack, chances are that automated bots are -probing for ways to fill your database with spam, links to malicious -software, and the like. +require attention to security. Some of these security concerns are +far more complex than one might think, and we all sometimes +underestimate the likelihood that a vulnerability will be exploited - +until a clever attacker figures out a way to exploit our applications. +And don't think that your application is not important enough to +attract an attacker. Depending on the kind of attack, chances are that +automated bots are probing for ways to fill your database with spam, +links to malicious software, and the like. So always keep security in mind when doing web development. -The Status of Python 3 +The Status of Python 3 ---------------------- -Currently the Python community is in the process of improving libraries to -support the new iteration of the Python programming language. While the -situation is greatly improving there are still some issues that make it -hard for us to switch over to Python 3 just now. These problems are -partially caused by changes in the language that went unreviewed for too -long, partially also because we have not quite worked out how the lower- -level API should change to account for the Unicode differences in Python 3. +Currently the Python community is in the process of improving +libraries to support the new iteration of the Python programming +language. While the situation is greatly improving there are still +some issues that make it hard for us to switch over to Python 3 just +now. These problems are partially caused by changes in the language +that went unreviewed for too long, partially also because we have not +quite worked out how the lower- level API should change to account for +the Unicode differences in Python 3. -Werkzeug and Flask will be ported to Python 3 as soon as a solution for -the changes is found, and we will provide helpful tips how to upgrade -existing applications to Python 3. Until then, we strongly recommend -using Python 2.6 and 2.7 with activated Python 3 warnings during -development. If you plan on upgrading to Python 3 in the near future we -strongly recommend that you read `How to write forwards compatible -Python code `_. +Werkzeug and Flask will be ported to Python 3 as soon as a solution +for the changes is found, and we will provide helpful tips how to +upgrade existing applications to Python 3. Until then, we strongly +recommend using Python 2.6 and 2.7 with activated Python 3 warnings +during development. If you plan on upgrading to Python 3 in the near +future we strongly recommend that you read `How to write forwards +compatible Python code `_. From c78070d8623fb6f40bf4ef20a1109083ca79ef7a Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 12 Mar 2012 15:08:52 -0700 Subject: [PATCH 0980/3747] Wrapped paragraphs; changed some words. --- docs/foreword.rst | 149 ++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/docs/foreword.rst b/docs/foreword.rst index 1fa214e6..5751ccb7 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -1,104 +1,99 @@ -Foreword +Foreword ======== -Read this before you get started with Flask. This hopefully answers -some questions about the purpose and goals of the project, and when -you should or should not be using it. +Read this before you get started with Flask. This hopefully answers some +questions about the purpose and goals of the project, and when you +should or should not be using it. -What does "micro" mean? +What does "micro" mean? ----------------------- -As Flask considers it, the "micro" in microframework refers not only -to the simplicity and small size of the framework, but also the fact -that it does not make many decisions for you. While Flask does pick a -templating engine for you, we won't make such decisions for your -datastore or other parts. +Flask considers the "micro" in microframework to refer not only to the +simplicity and small size of the framework, but also to the fact that it does +not make many decisions for you. While Flask does pick a templating engine +for you, we won't make such decisions for your datastore or other parts. -However, to us the term “micro” does not mean that the whole -implementation has to fit into a single Python file. +However, to us the term “micro” does not mean that the whole implementation +has to fit into a single Python file. One of the design decisions with Flask was that simple tasks should be -simple; they should not take a lot of code and yet they should not -limit you. Because of that we made a few design choices that some -people might find surprising or unorthodox. For example, Flask uses -thread-local objects internally so that you don't have to pass objects -around from function to function within a request in order to stay -threadsafe. While this is a really easy approach and saves you a lot -of time, it might also cause some troubles for very large applications -because changes on these thread-local objects can happen anywhere in -the same thread. In order to solve these problems we don't hide the -thread locals for you but instead embrace them and provide you with a -lot of tools to make it as pleasant as possible to work with them. +simple; they should not take a lot of code and yet they should not limit you. +Because of that we made a few design choices that some people might find +surprising or unorthodox. For example, Flask uses thread-local objects +internally so that you don't have to pass objects around from function to +function within a request in order to stay threadsafe. While this is a +really easy approach and saves you a lot of time, it might also cause some +troubles for very large applications because changes on these thread-local +objects can happen anywhere in the same thread. In order to solve these +problems we don't hide the thread locals for you but instead embrace them +and provide you with a lot of tools to make it as pleasant as possible to +work with them. Flask is also based on convention over configuration, which means that -many things are preconfigured. For example, by convention templates -and static files are stored in subdirectories within the application's -Python source tree. While this can be changed you usually don't have -to. +many things are preconfigured. For example, by convention templates and +static files are stored in subdirectories within the application's Python source tree. +While this can be changed you usually don't have to. -The main reason Flask is called a "microframework" is the idea to keep -the core simple but extensible. There is no database abstraction +The main reason Flask is called a "microframework" is the idea +to keep the core simple but extensible. There is no database abstraction layer, no form validation or anything else where different libraries -already exist that can handle that. However Flask supports extensions -to add such functionality to your application as if it was implemented -in Flask itself. There are currently extensions for object-relational -mappers, form validation, upload handling, various open authentication -technologies and more. +already exist that can handle that. However Flask supports +extensions to add such functionality to your application as if it +was implemented in Flask itself. There are currently extensions for +object-relational mappers, form validation, upload handling, various open +authentication technologies and more. -Since Flask is based on a very solid foundation there is not a lot of -code in Flask itself. As such it's easy to adapt even for large -applications and we are making sure that you can either configure it -as much as possible by subclassing things or by forking the entire -codebase. If you are interested in that, check out the -:ref:`becomingbig` chapter. +Since Flask is based on a very solid foundation there is not a lot of code +in Flask itself. As such it's easy to adapt even for large applications +and we are making sure that you can either configure it as much as +possible by subclassing things or by forking the entire codebase. If you +are interested in that, check out the :ref:`becomingbig` chapter. If you are curious about the Flask design principles, head over to the section about :ref:`design`. -Web Development is Dangerous ---------------------------- +Web Development is Dangerous +---------------------------- -If you write a web application, you are probably allowing users to -register and leave their data on your server. The users are -entrusting you with data. And even if you are the only user that -might leave data in your application, you still want that data to be -stored securely. +If you write a web application, you are probably allowing users to register +and leave their data on your server. The users are entrusting you with data. +And even if you are the only user that might leave data in your application, +you still want that data to be stored securely. -Unfortunately, there are many ways the security of a web application -can be compromised. Flask protects you against one of the most common -security problems of modern web applications: cross-site scripting -(XSS). Unless you deliberately mark insecure HTML as secure, Flask -and the underlying Jinja2 template engine have you covered. But there -are many more ways to cause security problems. +Unfortunately, there are many ways the security of a web application can be +compromised. Flask protects you against one of the most common security +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to +cause security problems. The documentation will warn you about aspects of web development that -require attention to security. Some of these security concerns are -far more complex than one might think, and we all sometimes -underestimate the likelihood that a vulnerability will be exploited - -until a clever attacker figures out a way to exploit our applications. -And don't think that your application is not important enough to -attract an attacker. Depending on the kind of attack, chances are that -automated bots are probing for ways to fill your database with spam, -links to malicious software, and the like. +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited - until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. So always keep security in mind when doing web development. -The Status of Python 3 +The Status of Python 3 ---------------------- -Currently the Python community is in the process of improving -libraries to support the new iteration of the Python programming -language. While the situation is greatly improving there are still -some issues that make it hard for us to switch over to Python 3 just -now. These problems are partially caused by changes in the language -that went unreviewed for too long, partially also because we have not -quite worked out how the lower- level API should change to account for -the Unicode differences in Python 3. +Currently the Python community is in the process of improving libraries to +support the new iteration of the Python programming language. While the +situation is greatly improving there are still some issues that make it +hard for us to switch over to Python 3 just now. These problems are +partially caused by changes in the language that went unreviewed for too +long, partially also because we have not quite worked out how the lower- +level API should change to account for the Unicode differences in Python 3. -Werkzeug and Flask will be ported to Python 3 as soon as a solution -for the changes is found, and we will provide helpful tips how to -upgrade existing applications to Python 3. Until then, we strongly -recommend using Python 2.6 and 2.7 with activated Python 3 warnings -during development. If you plan on upgrading to Python 3 in the near -future we strongly recommend that you read `How to write forwards -compatible Python code `_. +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +the changes is found, and we will provide helpful tips how to upgrade +existing applications to Python 3. Until then, we strongly recommend +using Python 2.6 and 2.7 with activated Python 3 warnings during +development. If you plan on upgrading to Python 3 in the near future we +strongly recommend that you read `How to write forwards compatible +Python code `_. From 756a5565ea395c5113e8e9cf21b39060548aa5ba Mon Sep 17 00:00:00 2001 From: wilsaj Date: Mon, 12 Mar 2012 17:21:49 -0500 Subject: [PATCH 0981/3747] docfix: wrong converter name: unicode -> string --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index ec7e4f63..fe871112 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -511,7 +511,7 @@ Variable parts are passed to the view function as keyword arguments. The following converters are available: =========== =============================================== -`unicode` accepts any text without a slash (the default) +`string` accepts any text without a slash (the default) `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes From a77938837c6466edfde7f1708ef56587189a5e2b Mon Sep 17 00:00:00 2001 From: wilsaj Date: Mon, 12 Mar 2012 17:21:49 -0500 Subject: [PATCH 0982/3747] docfix: wrong converter name: unicode -> string fixes #364 --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index ec7e4f63..fe871112 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -511,7 +511,7 @@ Variable parts are passed to the view function as keyword arguments. The following converters are available: =========== =============================================== -`unicode` accepts any text without a slash (the default) +`string` accepts any text without a slash (the default) `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes From 8d1546f8e64e093847ad3d5579ad5f9b7c3d0e45 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 12 Mar 2012 16:28:39 -0700 Subject: [PATCH 0983/3747] Reword the docs for writing a flask extension There was a minor bug in the example extension that's been fixed. I also updated the description of the fixed code accordingly, and expanded on the usage of _request_ctx_stack.top for adding data that should be accesible to view functions. I verified that the existing code as is works as expected. --- docs/extensiondev.rst | 149 ++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 85 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 074d06ab..ab038e0c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -125,9 +125,8 @@ Initializing Extensions ----------------------- Many extensions will need some kind of initialization step. For example, -consider your application is currently connecting to SQLite like the -documentation suggests (:ref:`sqlite3`) you will need to provide a few -functions and before / after request handlers. So how does the extension +consider an application that's currently connecting to SQLite like the +documentation suggests (:ref:`sqlite3`). So how does the extension know the name of the application object? Quite simple: you pass it to it. @@ -135,12 +134,14 @@ Quite simple: you pass it to it. There are two recommended ways for an extension to initialize: initialization functions: + If your extension is called `helloworld` you might have a function called ``init_helloworld(app[, extra_args])`` that initializes the extension for that application. It could attach before / after handlers etc. classes: + Classes work mostly like initialization functions but can later be used to further change the behaviour. For an example look at how the `OAuth extension`_ works: there is an `OAuth` object that provides @@ -148,92 +149,18 @@ classes: a remote application that uses OAuth. What to use depends on what you have in mind. For the SQLite 3 extension -we will use the class-based approach because it will provide users with a -manager object that handles opening and closing database connections. +we will use the class-based approach because it will provide users with an +object that handles opening and closing database connections. The Extension Code ------------------ Here's the contents of the `flask_sqlite3.py` for copy/paste:: - from __future__ import absolute_import import sqlite3 from flask import _request_ctx_stack - class SQLite3(object): - - def __init__(self, app): - self.app = app - self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.teardown_request(self.teardown_request) - self.app.before_request(self.before_request) - - def connect(self): - return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) - - def before_request(self): - ctx = _request_ctx_stack.top - ctx.sqlite3_db = self.connect() - - def teardown_request(self, exception): - ctx = _request_ctx_stack.top - ctx.sqlite3_db.close() - - def get_db(self): - ctx = _request_ctx_stack.top - if ctx is not None: - return ctx.sqlite3_db - -So here's what these lines of code do: - -1. The ``__future__`` import is necessary to activate absolute imports. - Otherwise we could not call our module `sqlite3.py` and import the - top-level `sqlite3` module which actually implements the connection to - SQLite. -2. We create a class for our extension that requires a supplied `app` object, - sets a configuration for the database if it's not there - (:meth:`dict.setdefault`), and attaches `before_request` and - `teardown_request` handlers. -3. Next, we define a `connect` function that opens a database connection. -4. Then we set up the request handlers we bound to the app above. Note here - that we're attaching our database connection to the top request context via - `_request_ctx_stack.top`. Extensions should use the top context and not the - `g` object to store things like database connections. -5. Finally, we add a `get_db` function that simplifies access to the context's - database. - -So why did we decide on a class-based approach here? Because using our -extension looks something like this:: - - from flask import Flask - from flask_sqlite3 import SQLite3 - - app = Flask(__name__) - app.config.from_pyfile('the-config.cfg') - manager = SQLite3(app) - db = manager.get_db() - -You can then use the database from views like this:: - - @app.route('/') - def show_all(): - cur = db.cursor() - cur.execute(...) - -Opening a database connection from outside a view function is simple. - ->>> from yourapplication import db ->>> cur = db.cursor() ->>> cur.execute(...) - -Adding an `init_app` Function ------------------------------ - -In practice, you'll almost always want to permit users to initialize your -extension and provide an app object after the fact. This can help avoid -circular import problems when a user is breaking their app into multiple files. -Our extension could add an `init_app` function as follows:: class SQLite3(object): @@ -251,7 +178,7 @@ Our extension could add an `init_app` function as follows:: self.app.before_request(self.before_request) def connect(self): - return sqlite3.connect(app.config['SQLITE3_DATABASE']) + return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) def before_request(self): ctx = _request_ctx_stack.top @@ -261,18 +188,69 @@ Our extension could add an `init_app` function as follows:: ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - def get_db(self): + @property + def connection(self): ctx = _request_ctx_stack.top if ctx is not None: return ctx.sqlite3_db -The user could then initialize the extension in one file:: - manager = SQLite3() +So here's what these lines of code do: -and bind their app to the extension in another file:: +1. The ``__init__`` method takes an optional app object and, if supplied, will + call ``init_app``. +2. The ``init_app`` method exists so that the ``SQLite3`` object can be + instantiated without requiring an app object. This method supports the + factory pattern for creating applications. The ``init_app`` will set the + configuration for the database, defaulting to an in memory database if + no configuration is supplied. In addition, the ``init_app`` method attaches + ``before_request`` and ``teardown_request`` handlers. +3. Next, we define a ``connect`` method that opens a database connection. +4. Then we set up the request handlers we bound to the app above. Note here + that we're attaching our database connection to the top request context via + ``_request_ctx_stack.top``. Extensions should use the top context and not the + ``g`` object to store things like database connections. +5. Finally, we add a ``connection`` property that simplifies access to the context's + database. - manager.init_app(app) +So why did we decide on a class-based approach here? Because using our +extension looks something like this:: + + from flask import Flask + from flask_sqlite3 import SQLite3 + + app = Flask(__name__) + app.config.from_pyfile('the-config.cfg') + db = SQLite3(app) + +You can then use the database from views like this:: + + @app.route('/') + def show_all(): + cur = db.connection.cursor() + cur.execute(...) + +Additionally, the ``init_app`` method is used to support the factory pattern +for creating apps:: + + db = Sqlite3() + # Then later on. + app = create_app('the-config.cfg') + db.init_app(app) + +Keep in mind that supporting this factory pattern for creating apps is required +for approved flask extensions (described below). + + +Using _request_ctx_stack +------------------------ + +In the example above, before every request, a ``sqlite3_db`` variable is assigned +to ``_request_ctx_stack.top``. In a view function, this variable is accessible +using the ``connection`` property of ``SQLite3``. During the teardown of a +request, the ``sqlite3_db`` connection is closed. By using this pattern, the +*same* connection to the sqlite3 database is accessible to anything that needs it +for the duration of the request. End-Of-Request Behavior ----------------------- @@ -292,6 +270,7 @@ pattern is a good way to support both:: else: app.after_request(close_connection) + Strictly speaking the above code is wrong, because teardown functions are passed the exception and typically don't return anything. However because the return value is discarded this will just work assuming that the code From 8f568cfc19f5b5f2aa59b06d4e2b5b8d31423605 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 12 Mar 2012 17:12:55 -0700 Subject: [PATCH 0984/3747] Split foreword into two files; edited lots. --- docs/advanced_foreword.rst | 67 +++++++++++++++++++++ docs/foreword.rst | 118 ++++++++++++------------------------- 2 files changed, 104 insertions(+), 81 deletions(-) create mode 100644 docs/advanced_foreword.rst diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst new file mode 100644 index 00000000..cc1a1843 --- /dev/null +++ b/docs/advanced_foreword.rst @@ -0,0 +1,67 @@ +Foreword for Experienced Programmers +==================================== + +This chapter is for programmers who have worked with other frameworks in the +past, and who may have more specific or esoteric concerns that the typical +user. + +Threads in Flask +---------------- + +One of the design decisions with Flask was that simple tasks should be simple; +they should not take a lot of code and yet they should not limit you. Because +of that we made a few design choices that some people might find surprising or +unorthodox. For example, Flask uses thread-local objects internally so that +you don’t have to pass objects around from function to function within a +request in order to stay threadsafe. While this is a really easy approach and +saves you a lot of time, it might also cause some troubles for very large +applications because changes on these thread-local objects can happen anywhere +in the same thread. In order to solve these problems we don’t hide the thread +locals for you but instead embrace them and provide you with a lot of tools to +make it as pleasant as possible to work with them. + +Web Development is Dangerous +---------------------------- + +If you write a web application, you are probably allowing users to register +and leave their data on your server. The users are entrusting you with data. +And even if you are the only user that might leave data in your application, +you still want that data to be stored securely. + +Unfortunately, there are many ways the security of a web application can be +compromised. Flask protects you against one of the most common security +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to +cause security problems. + +The documentation will warn you about aspects of web development that +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited - until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. + +So always keep security in mind when doing web development. + +The Status of Python 3 +---------------------- + +Currently the Python community is in the process of improving libraries to +support the new iteration of the Python programming language. While the +situation is greatly improving there are still some issues that make it +hard for us to switch over to Python 3 just now. These problems are +partially caused by changes in the language that went unreviewed for too +long, partially also because we have not quite worked out how the lower- +level API should change to account for the Unicode differences in Python 3. + +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +the changes is found, and we will provide helpful tips how to upgrade +existing applications to Python 3. Until then, we strongly recommend +using Python 2.6 and 2.7 with activated Python 3 warnings during +development. If you plan on upgrading to Python 3 in the near future we +strongly recommend that you read `How to write forwards compatible +Python code `_. diff --git a/docs/foreword.rst b/docs/foreword.rst index 5751ccb7..b186aba6 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -8,92 +8,48 @@ should or should not be using it. What does "micro" mean? ----------------------- -Flask considers the "micro" in microframework to refer not only to the -simplicity and small size of the framework, but also to the fact that it does -not make many decisions for you. While Flask does pick a templating engine -for you, we won't make such decisions for your datastore or other parts. +“Micro” does not mean that your whole web application has to fit into +a single Python file (although it certainly can). Nor does it mean +that Flask is lacking in functionality. The "micro" in microframework +means Flask aims to keep the core simple but extensible. Flask won't make +many decisions for you, such as what database to use. Those decisions that +it does make, such as what templating engine to use, are easy to change. +Everything else is up to you, so that Flask can be everything you need +and nothing you don't. -However, to us the term “micro” does not mean that the whole implementation -has to fit into a single Python file. +By default, Flask does not include a database abstraction layer, form +validation or anything else where different libraries already exist that can +handle that. Instead, FLask extensions add such functionality to your +application as if it was implemented in Flask itself. Numerous extensions +provide database integration, form validation, upload handling, various open +authentication technologies, and more. Flask may be "micro", but the +possibilities are endless. -One of the design decisions with Flask was that simple tasks should be -simple; they should not take a lot of code and yet they should not limit you. -Because of that we made a few design choices that some people might find -surprising or unorthodox. For example, Flask uses thread-local objects -internally so that you don't have to pass objects around from function to -function within a request in order to stay threadsafe. While this is a -really easy approach and saves you a lot of time, it might also cause some -troubles for very large applications because changes on these thread-local -objects can happen anywhere in the same thread. In order to solve these -problems we don't hide the thread locals for you but instead embrace them -and provide you with a lot of tools to make it as pleasant as possible to -work with them. +Convention over Configuration +----------------------------- -Flask is also based on convention over configuration, which means that -many things are preconfigured. For example, by convention templates and -static files are stored in subdirectories within the application's Python source tree. -While this can be changed you usually don't have to. +Flask is based on convention over configuration, which means that many things +are preconfigured. For example, by convention templates and static files are +stored in subdirectories within the application's Python source tree. While +this can be changed you usually don't have to. We want to minimize the time +you need to spend in order to get up and running, without assuming things +about your needs. -The main reason Flask is called a "microframework" is the idea -to keep the core simple but extensible. There is no database abstraction -layer, no form validation or anything else where different libraries -already exist that can handle that. However Flask supports -extensions to add such functionality to your application as if it -was implemented in Flask itself. There are currently extensions for -object-relational mappers, form validation, upload handling, various open -authentication technologies and more. +Growing Up +---------- -Since Flask is based on a very solid foundation there is not a lot of code -in Flask itself. As such it's easy to adapt even for large applications -and we are making sure that you can either configure it as much as -possible by subclassing things or by forking the entire codebase. If you -are interested in that, check out the :ref:`becomingbig` chapter. +Since Flask is based on a very solid foundation there is not a lot of code in +Flask itself. As such it's easy to adapt even for large applications and we +are making sure that you can either configure it as much as possible by +subclassing things or by forking the entire codebase. If you are interested +in that, check out the :ref:`becomingbig` chapter. -If you are curious about the Flask design principles, head over to the -section about :ref:`design`. +If you are curious about the Flask design principles, head over to the section +about :ref:`design`. -Web Development is Dangerous ----------------------------- +For the Stalwart and Wizened... +------------------------------- -If you write a web application, you are probably allowing users to register -and leave their data on your server. The users are entrusting you with data. -And even if you are the only user that might leave data in your application, -you still want that data to be stored securely. - -Unfortunately, there are many ways the security of a web application can be -compromised. Flask protects you against one of the most common security -problems of modern web applications: cross-site scripting (XSS). Unless -you deliberately mark insecure HTML as secure, Flask and the underlying -Jinja2 template engine have you covered. But there are many more ways to -cause security problems. - -The documentation will warn you about aspects of web development that -require attention to security. Some of these security concerns -are far more complex than one might think, and we all sometimes underestimate -the likelihood that a vulnerability will be exploited - until a clever -attacker figures out a way to exploit our applications. And don't think -that your application is not important enough to attract an attacker. -Depending on the kind of attack, chances are that automated bots are -probing for ways to fill your database with spam, links to malicious -software, and the like. - -So always keep security in mind when doing web development. - -The Status of Python 3 ----------------------- - -Currently the Python community is in the process of improving libraries to -support the new iteration of the Python programming language. While the -situation is greatly improving there are still some issues that make it -hard for us to switch over to Python 3 just now. These problems are -partially caused by changes in the language that went unreviewed for too -long, partially also because we have not quite worked out how the lower- -level API should change to account for the Unicode differences in Python 3. - -Werkzeug and Flask will be ported to Python 3 as soon as a solution for -the changes is found, and we will provide helpful tips how to upgrade -existing applications to Python 3. Until then, we strongly recommend -using Python 2.6 and 2.7 with activated Python 3 warnings during -development. If you plan on upgrading to Python 3 in the near future we -strongly recommend that you read `How to write forwards compatible -Python code `_. +If you're more curious about the minutiae of Flask's implementation, and +whether its structure is right for your needs, read the +:ref:`advanced_foreword`. From 3bf1750b5dfde8890eab52850bf2e6c0a3de65cf Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Mar 2012 12:12:47 -0700 Subject: [PATCH 0985/3747] Tighten quickstart deployment docs. --- docs/deploying/index.rst | 3 +++ docs/quickstart.rst | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index d258df89..1b4189c3 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -13,6 +13,9 @@ If you have a different WSGI server look up the server documentation about how to use a WSGI app with it. Just remember that your :class:`Flask` application object is the actual WSGI application. +For hosted options to get up and running quickly, see +:ref:`quickstart_deployment` in the Quickstart. + .. toctree:: :maxdepth: 2 diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8ff7f3cf..0d8c5b73 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -827,18 +827,25 @@ can do it like this:: from werkzeug.contrib.fixers import LighttpdCGIRootFix app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) +.. _quickstart_deployment: + Deploying to a Web Server ------------------------- -If you want to make your Flask app available to the Internet at large, `Heroku -`_ is very easy to set up and will run small Flask -applications for free. `Check out their tutorial on how to deploy Flask apps on -their service `_. - -There are a number of other websites that will host your Flask app and make it -easy for you to do so. +Ready to deploy your new Flask app? To wrap up the quickstart, you can +immediately deploy to a hosted platform, all of which offer a free plan for +small projects: +- `Deploying Flask on Heroku `_ - `Deploying Flask on ep.io `_ +- `Deploying WSGI on dotCloud `_ + with `Flask-specific notes `_ + +Other places where you can host your Flask app: + - `Deploying Flask on Webfaction `_ - `Deploying Flask on Google App Engine `_ - `Sharing your Localhost Server with Localtunnel `_ + +If you manage your own hosts and would like to host yourself, see the chapter +on :ref:`deployment`. From c1a2e3cf1479382c1d1e5c46cd2d1ca669df5889 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Mar 2012 13:41:03 -0700 Subject: [PATCH 0986/3747] Add Rule #0 to extension development. --- docs/extensiondev.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 5a8d5b16..d997b2de 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -326,6 +326,10 @@ new releases. These approved extensions are listed on the `Flask Extension Registry`_ and marked appropriately. If you want your own extension to be approved you have to follow these guidelines: +0. An approved Flask extension requires a maintainer. In the event an + extension author would like to move beyond the project, the project should + find a new maintainer including full source hosting transition and PyPI + access. If no maintainer is available, give access to the Flask core team. 1. An approved Flask extension must provide exactly one package or module named ``flask_extensionname``. They might also reside inside a ``flaskext`` namespace packages though this is discouraged now. From 146088d58066f16ef4bc8172f8120402517c34d3 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Mar 2012 14:37:48 -0700 Subject: [PATCH 0987/3747] Expand docs on send_file option hook, #433. --- CHANGES | 4 ++++ flask/helpers.py | 18 ++++++++++++++++-- flask/testsuite/helpers.py | 4 +--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index dbf447db..6b96be9e 100644 --- a/CHANGES +++ b/CHANGES @@ -48,6 +48,10 @@ Relase date to be decided, codename to be chosen. - View functions can now return a tuple with the first instance being an instance of :class:`flask.Response`. This allows for returning ``jsonify(error="error msg"), 400`` from a view function. +- :class:`flask.Flask` now provides a `get_static_file_options` hook for + subclasses to override behavior of serving static files through Flask, + optionally by filename, which for example allows changing cache controls by + file extension. Version 0.8.1 diff --git a/flask/helpers.py b/flask/helpers.py index 4cb918d2..9964792b 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -653,8 +653,22 @@ class _PackageBoundObject(object): self.template_folder)) def get_static_file_options(self, filename): - """Function used internally to determine what keyword arguments - to send to :func:`send_from_directory` for a specific file.""" + """Provides keyword arguments to send to :func:`send_from_directory`. + + This allows subclasses to change the behavior when sending files based + on the filename. For example, to set the cache timeout for .js files + to 60 seconds (note the options are keywords for :func:`send_file`):: + + class MyFlask(flask.Flask): + def get_static_file_options(self, filename): + options = super(MyFlask, self).get_static_file_options(filename) + if filename.lower().endswith('.js'): + options['cache_timeout'] = 60 + options['conditional'] = True + return options + + .. versionaded:: 0.9 + """ return {} def send_static_file(self, filename): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index c88026d9..42331993 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -213,8 +213,6 @@ class SendfileTestCase(FlaskTestCase): self.assert_equal(cc.max_age, 12 * 60 * 60) # override get_static_file_options with some new values and check them class StaticFileApp(flask.Flask): - def __init__(self): - super(StaticFileApp, self).__init__(__name__) def get_static_file_options(self, filename): opts = super(StaticFileApp, self).get_static_file_options(filename) opts['cache_timeout'] = 10 @@ -222,7 +220,7 @@ class SendfileTestCase(FlaskTestCase): # keyword arg in the guts opts['conditional'] = True return opts - app = StaticFileApp() + app = StaticFileApp(__name__) with app.test_request_context(): rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) From d94efc6db63516b7f72e58c34ae33700f3d9c4fb Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Mar 2012 16:34:16 -0700 Subject: [PATCH 0988/3747] Expose send_file max-age as config value, #433. Need to add the same hook in a Blueprint, but this is the first such case where we need app.config in the Blueprint. --- CHANGES | 12 ++++++++---- docs/config.rst | 9 ++++++++- flask/app.py | 7 +++++++ flask/helpers.py | 14 +++++++++----- flask/testsuite/blueprints.py | 14 ++++++++++++++ flask/testsuite/helpers.py | 13 +++++++++---- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/CHANGES b/CHANGES index 6b96be9e..ee029adc 100644 --- a/CHANGES +++ b/CHANGES @@ -48,10 +48,14 @@ Relase date to be decided, codename to be chosen. - View functions can now return a tuple with the first instance being an instance of :class:`flask.Response`. This allows for returning ``jsonify(error="error msg"), 400`` from a view function. -- :class:`flask.Flask` now provides a `get_static_file_options` hook for - subclasses to override behavior of serving static files through Flask, - optionally by filename, which for example allows changing cache controls by - file extension. +- :class:`flask.Flask` now provides a `get_send_file_options` hook for + subclasses to override behavior of serving static files from Flask when using + :meth:`flask.Flask.send_static_file` based on keywords in + :func:`flask.helpers.send_file`. This hook is provided a filename, which for + example allows changing cache controls by file extension. The default + max-age for `send_static_file` can be configured through a new + ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, regardless of whether + the `get_send_file_options` hook is used. Version 0.8.1 diff --git a/docs/config.rst b/docs/config.rst index 2f9d8307..cf3c6a4a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -107,6 +107,13 @@ The following configuration values are used internally by Flask: reject incoming requests with a content length greater than this by returning a 413 status code. +``SEND_FILE_MAX_AGE_DEFAULT``: Default cache control max age to use with + :meth:`flask.Flask.send_static_file`, in + seconds. Override this value on a per-file + basis using the + :meth:`flask.Flask.get_send_file_options` and + :meth:`flask.Blueprint.get_send_file_options` + hooks. Defaults to 43200 (12 hours). ``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will not execute the error handlers of HTTP exceptions but instead treat the @@ -267,7 +274,7 @@ configuration:: class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' - + class DevelopmentConfig(Config): DEBUG = True diff --git a/flask/app.py b/flask/app.py index f3d7efcb..0876ac64 100644 --- a/flask/app.py +++ b/flask/app.py @@ -249,6 +249,7 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, + 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False }) @@ -1020,6 +1021,12 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .append((code_or_exception, f)) + def get_send_file_options(self, filename): + # Override: Hooks in SEND_FILE_MAX_AGE_DEFAULT config. + options = super(Flask, self).get_send_file_options(filename) + options['cache_timeout'] = self.config['SEND_FILE_MAX_AGE_DEFAULT'] + return options + @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. diff --git a/flask/helpers.py b/flask/helpers.py index 9964792b..52b0cebc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -319,6 +319,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, guessing requires a `filename` or an `attachment_filename` to be provided. + Note `get_send_file_options` in :class:`flask.Flask` hooks the + ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable to set the default + cache_timeout. + Please never pass filenames to this function from user sources without checking them first. Something like this is usually sufficient to avoid security problems:: @@ -652,7 +656,7 @@ class _PackageBoundObject(object): return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) - def get_static_file_options(self, filename): + def get_send_file_options(self, filename): """Provides keyword arguments to send to :func:`send_from_directory`. This allows subclasses to change the behavior when sending files based @@ -660,14 +664,14 @@ class _PackageBoundObject(object): to 60 seconds (note the options are keywords for :func:`send_file`):: class MyFlask(flask.Flask): - def get_static_file_options(self, filename): - options = super(MyFlask, self).get_static_file_options(filename) + def get_send_file_options(self, filename): + options = super(MyFlask, self).get_send_file_options(filename) if filename.lower().endswith('.js'): options['cache_timeout'] = 60 options['conditional'] = True return options - .. versionaded:: 0.9 + .. versionadded:: 0.9 """ return {} @@ -680,7 +684,7 @@ class _PackageBoundObject(object): if not self.has_static_folder: raise RuntimeError('No static folder for this object') return send_from_directory(self.static_folder, filename, - **self.get_static_file_options(filename)) + **self.get_send_file_options(filename)) def open_resource(self, resource, mode='rb'): """Opens a resource from the application's resource folder. To see diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 5bf81d92..5f3d3ab3 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -16,6 +16,7 @@ import unittest import warnings from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import NotFound +from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound @@ -357,6 +358,19 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/admin/static/css/test.css') self.assert_equal(rv.data.strip(), '/* nested file */') + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT'] + try: + expected_max_age = 3600 + if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age: + expected_max_age = 7200 + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age + rv = c.get('/admin/static/css/test.css') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, expected_max_age) + finally: + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default + with app.test_request_context(): self.assert_equal(flask.url_for('admin.static', filename='test.txt'), '/admin/static/test.txt') diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 42331993..b4dd00ea 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -206,15 +206,20 @@ class SendfileTestCase(FlaskTestCase): def test_static_file(self): app = flask.Flask(__name__) - # default cache timeout is 12 hours (hard-coded) + # default cache timeout is 12 hours with app.test_request_context(): rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 12 * 60 * 60) - # override get_static_file_options with some new values and check them + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600 + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 3600) + # override get_send_file_options with some new values and check them class StaticFileApp(flask.Flask): - def get_static_file_options(self, filename): - opts = super(StaticFileApp, self).get_static_file_options(filename) + def get_send_file_options(self, filename): + opts = super(StaticFileApp, self).get_send_file_options(filename) opts['cache_timeout'] = 10 # this test catches explicit inclusion of the conditional # keyword arg in the guts From 73cb15ed2cb208381b31e7f868adfb4117cc803d Mon Sep 17 00:00:00 2001 From: iammookli Date: Thu, 15 Mar 2012 18:20:17 -0700 Subject: [PATCH 0989/3747] Update docs/patterns/mongokit.rst --- docs/patterns/mongokit.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index a9c4eef5..b50cf456 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -141,4 +141,4 @@ These results are also dict-like objects: u'admin@localhost' For more information about MongoKit, head over to the -`website `_. +`website `_. From fe9f5a47687cccaaaf13f160d747ce8b4c03bad9 Mon Sep 17 00:00:00 2001 From: jtsoi Date: Fri, 16 Mar 2012 09:38:40 +0100 Subject: [PATCH 0990/3747] Added an example of how to configure debugging with run_simple, it has to be enabled both for the Flask app and the Werkzeug server. --- docs/patterns/appdispatch.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 177ade2b..c48d3c28 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -30,6 +30,23 @@ at :func:`werkzeug.serving.run_simple`:: Note that :func:`run_simple ` is not intended for use in production. Use a :ref:`full-blown WSGI server `. +In order to use the interactive debuggger, debugging must be enables both on +the application and the simple server, here is the "hello world" example with debugging and +:func:`run_simple ` : + + from flask import Flask + from werkzeug.serving import run_simple + + app = Flask(__name__) + app.debug = True + + @app.route('/') + def hello_world(): + return 'Hello World!' + + if __name__ == '__main__': + run_simple('localhost', 5000, app, use_reloader=True, use_debugger=True, use_evalex=True) + Combining Applications ---------------------- From ee6ed491d3a783076c278e4b4390baf14e6f3321 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Mon, 19 Mar 2012 00:52:33 +0100 Subject: [PATCH 0991/3747] Have tox install simplejson for python 2.5 --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 91c6d664..82c1588e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,3 +3,6 @@ envlist=py25,py26,py27,pypy [testenv] commands=python run-tests.py + +[testenv:py25] +deps=simplejson From bb99158c870a2d761f1349af02ef1decf0d10c7b Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 19 Mar 2012 22:33:43 -0700 Subject: [PATCH 0992/3747] Remove an unused iteration variable. We can just iterate over the namespace dictionary's keys here. We don't need its values. --- flask/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index 79d62992..5192c1c1 100644 --- a/flask/views.py +++ b/flask/views.py @@ -107,7 +107,7 @@ class MethodViewType(type): rv = type.__new__(cls, name, bases, d) if 'methods' not in d: methods = set(rv.methods or []) - for key, value in d.iteritems(): + for key in d: if key in http_method_funcs: methods.add(key.upper()) # if we have no method at all in there we don't want to From c2661dd4bcb41e5a4c47709a8be7704870aba0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Risti=C4=87?= Date: Tue, 20 Mar 2012 22:07:58 +0100 Subject: [PATCH 0993/3747] Update docs/patterns/packages.rst --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 79fd2c58..1c7f9bd0 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -55,7 +55,7 @@ following quick checklist: `__init__.py` file. That way each module can import it safely and the `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` - decorator on top) have to be imported when in the `__init__.py` file. + decorator on top) have to be imported in the `__init__.py` file. Not the object itself, but the module it is in. Import the view module **after the application object is created**. From b29834dac37a13f82019aa1c7e9d06622cf5790e Mon Sep 17 00:00:00 2001 From: Kamil Kisiel Date: Sat, 24 Mar 2012 14:09:43 -0700 Subject: [PATCH 0994/3747] Fixed weird string quoting in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 812b2c8a..8169a517 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ class run_audit(Command): try: import pyflakes.scripts.pyflakes as flakes except ImportError: - print "Audit requires PyFlakes installed in your system.""" + print "Audit requires PyFlakes installed in your system." sys.exit(-1) warns = 0 From e08028de5521caf41fc11de8daec2795f1f51088 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 27 Mar 2012 17:08:55 +0300 Subject: [PATCH 0995/3747] pip vs. easy_install consistency --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 8e6a4497..791c99f1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -94,7 +94,7 @@ System-Wide Installation ------------------------ This is possible as well, though I do not recommend it. Just run -`easy_install` with root privileges:: +`pip` with root privileges:: $ sudo pip install Flask From 35383ee83c568ce642ffef6733d8b91ebd206185 Mon Sep 17 00:00:00 2001 From: Pascal Hartig Date: Wed, 28 Mar 2012 10:33:27 +0300 Subject: [PATCH 0996/3747] Removed triple-quotes from print statement in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 812b2c8a..8169a517 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ class run_audit(Command): try: import pyflakes.scripts.pyflakes as flakes except ImportError: - print "Audit requires PyFlakes installed in your system.""" + print "Audit requires PyFlakes installed in your system." sys.exit(-1) warns = 0 From e76db15e26b76bdaed4649474f6509e383142e9c Mon Sep 17 00:00:00 2001 From: Sergey Date: Sat, 31 Mar 2012 10:11:12 +0300 Subject: [PATCH 0997/3747] Update docs/quickstart.rst --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0d8c5b73..8f38aff5 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -193,7 +193,7 @@ The following converters exist: with a trailing slash will produce a 404 "Not Found" error. This behavior allows relative URLs to continue working if users access the - page when they forget a trailing slash, consistent with how with how Apache + page when they forget a trailing slash, consistent with how Apache and other servers work. Also, the URLs will stay unique, which helps search engines avoid indexing the same page twice. From 7e4b705b3c7124bc5bdd4051705488d8bb31eb5b Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 10:54:00 -0400 Subject: [PATCH 0998/3747] Move others.rst to wsgi-standalone.rst. --- docs/deploying/{others.rst => wsgi-standalone.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/deploying/{others.rst => wsgi-standalone.rst} (100%) diff --git a/docs/deploying/others.rst b/docs/deploying/wsgi-standalone.rst similarity index 100% rename from docs/deploying/others.rst rename to docs/deploying/wsgi-standalone.rst From 976c9576bd7b41f955862448c9914774dc47d1cf Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 10:54:27 -0400 Subject: [PATCH 0999/3747] Reorder deployment options. --- docs/deploying/fastcgi.rst | 11 +++---- docs/deploying/index.rst | 6 ++-- docs/deploying/uwsgi.rst | 9 +++-- docs/deploying/wsgi-standalone.rst | 53 ++++++++++++++++-------------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 6dace1a8..ebd68560 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -3,12 +3,11 @@ FastCGI ======= -FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, -and `cherokee`_; see :ref:`deploying-uwsgi` and -:ref:`deploying-other-servers` for other options. To use your WSGI -application with any of them you will need a FastCGI server first. The -most popular one is `flup`_ which we will use for this guide. Make sure -to have it installed to follow along. +FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, and +`cherokee`_; see :ref:`deploying-uwsgi` and :ref:`deploying-wsgi-standalone` +for other options. To use your WSGI application with any of them you will need +a FastCGI server first. The most popular one is `flup`_ which we will use for +this guide. Make sure to have it installed to follow along. .. admonition:: Watch Out diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 1b4189c3..bf78275d 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -20,7 +20,7 @@ For hosted options to get up and running quickly, see :maxdepth: 2 mod_wsgi - cgi - fastcgi + wsgi-standalone uwsgi - others + fastcgi + cgi diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index bdee15ba..b05fdeec 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -4,11 +4,10 @@ uWSGI ===== uWSGI is a deployment option on servers like `nginx`_, `lighttpd`_, and -`cherokee`_; see :ref:`deploying-fastcgi` and -:ref:`deploying-other-servers` for other options. To use your WSGI -application with uWSGI protocol you will need a uWSGI server -first. uWSGI is both a protocol and an application server; the -application server can serve uWSGI, FastCGI, and HTTP protocols. +`cherokee`_; see :ref:`deploying-fastcgi` and :ref:`deploying-wsgi-standalone` +for other options. To use your WSGI application with uWSGI protocol you will +need a uWSGI server first. uWSGI is both a protocol and an application server; +the application server can serve uWSGI, FastCGI, and HTTP protocols. The most popular uWSGI server is `uwsgi`_, which we will use for this guide. Make sure to have it installed to follow along. diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index 6f3e5cc6..4bb985d4 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -1,11 +1,31 @@ -.. _deploying-other-servers: +.. _deploying-wsgi-standalone: -Other Servers -============= +Standalone WSGI Containers +========================== -There are popular servers written in Python that allow the execution of WSGI -applications as well. These servers stand alone when they run; you can proxy -to them from your web server. +There are popular servers written in Python that contain WSGI applications and +serve HTTP. These servers stand alone when they run; you can proxy to them +from your web server. Note the section on :ref:`deploying-proxy-setups` if you +run into issues. + +Gunicorn +-------- + +`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork +worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ +and `greenlet`_. Running a Flask application on this server is quite simple:: + + gunicorn myproject:app + +`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. +For example, to run a Flask application with 4 worker processes (``-w +4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: + + gunicorn -w 4 -b 127.0.0.1:4000 myproject:app + +.. _Gunicorn: http://gunicorn.org/ +.. _eventlet: http://eventlet.net/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html Tornado -------- @@ -14,7 +34,7 @@ Tornado server and tools that power `FriendFeed`_. Because it is non-blocking and uses epoll, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. Integrating this -service with Flask is a trivial task:: +service with Flask is straightforward:: from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer @@ -46,24 +66,7 @@ event loop:: .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ -Gunicorn --------- - -`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork -worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ -and `greenlet`_. Running a Flask application on this server is quite simple:: - - gunicorn myproject:app - -`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. -For example, to run a Flask application with 4 worker processes (``-w -4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: - - gunicorn -w 4 -b 127.0.0.1:4000 myproject:app - -.. _Gunicorn: http://gunicorn.org/ -.. _eventlet: http://eventlet.net/ -.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html +.. _deploying-proxy-setups: Proxy Setups ------------ From 9a1d616706d251b19571d908282deadedd89869b Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 10:57:44 -0400 Subject: [PATCH 1000/3747] Add simple proxying nginx config to docs. Origin: https://gist.github.com/2269917 --- docs/deploying/wsgi-standalone.rst | 34 ++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index 4bb985d4..422a9340 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -71,12 +71,34 @@ event loop:: Proxy Setups ------------ -If you deploy your application using one of these servers behind an HTTP -proxy you will need to rewrite a few headers in order for the -application to work. The two problematic values in the WSGI environment -usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that -will solve some common setups, but you might want to write your own WSGI -middleware for specific setups. +If you deploy your application using one of these servers behind an HTTP proxy +you will need to rewrite a few headers in order for the application to work. +The two problematic values in the WSGI environment usually are `REMOTE_ADDR` +and `HTTP_HOST`. You can configure your httpd to pass these headers, or you +can fix them in middleware. Werkzeug ships a fixer that will solve some common +setups, but you might want to write your own WSGI middleware for specific +setups. + +Here's a simple nginx configuration which proxies to an application served on +localhost at port 8000, setting appropriate headers:: + + server { + listen 80; + + server_name _; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + location / { + proxy_pass http://127.0.0.1:8000/; + proxy_redirect off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } The most common setup invokes the host being set from `X-Forwarded-Host` and the remote address from `X-Forwarded-For`:: From 9ab41edbd727d69d6936b866ea606c9b7e7bac8f Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 11:19:51 -0400 Subject: [PATCH 1001/3747] Touch up proxying docs. --- docs/deploying/wsgi-standalone.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index 422a9340..74385813 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -80,7 +80,9 @@ setups, but you might want to write your own WSGI middleware for specific setups. Here's a simple nginx configuration which proxies to an application served on -localhost at port 8000, setting appropriate headers:: +localhost at port 8000, setting appropriate headers: + +.. sourcecode:: nginx server { listen 80; @@ -100,15 +102,18 @@ localhost at port 8000, setting appropriate headers:: } } -The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forwarded-For`:: +If your httpd is not providing these headers, the most common setup invokes the +host being set from `X-Forwarded-Host` and the remote address from +`X-Forwarded-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) -Please keep in mind that it is a security issue to use such a middleware -in a non-proxy setup because it will blindly trust the incoming -headers which might be forged by malicious clients. +.. admonition:: Trusting Headers + + Please keep in mind that it is a security issue to use such a middleware in + a non-proxy setup because it will blindly trust the incoming headers which + might be forged by malicious clients. If you want to rewrite the headers from another header, you might want to use a fixer like this:: From 0eb75b317bd62ece31875158fb31262ce7d05e69 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 11:33:42 -0400 Subject: [PATCH 1002/3747] Add notes on mutable values & sessions. Using notes in 8445f0d939dc3c4a2e722dc6dd4938d02bc2e094 --- CHANGES | 3 ++- flask/helpers.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index ee029adc..bee0ab77 100644 --- a/CHANGES +++ b/CHANGES @@ -56,7 +56,8 @@ Relase date to be decided, codename to be chosen. max-age for `send_static_file` can be configured through a new ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, regardless of whether the `get_send_file_options` hook is used. - +- Fixed an assumption in sessions implementation which could break message + flashing on sessions implementations which use external storage. Version 0.8.1 ------------- diff --git a/flask/helpers.py b/flask/helpers.py index e6fb4ae3..294c5297 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -283,6 +283,13 @@ def flash(message, category='message'): messages and ``'warning'`` for warnings. However any kind of string can be used as category. """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # are always in sync with the sess on object, which is not true for session + # implementations that use external storage for keeping their keys/values. flashes = session.get('_flashes', []) flashes.append((category, message)) session['_flashes'] = flashes From df772df24f22f7f0681a8d5b211dad764ce9c8a6 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Sun, 1 Apr 2012 11:40:37 -0400 Subject: [PATCH 1003/3747] Touch up run_simple doc, #446. --- docs/patterns/appdispatch.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index c48d3c28..a2d1176f 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -30,9 +30,9 @@ at :func:`werkzeug.serving.run_simple`:: Note that :func:`run_simple ` is not intended for use in production. Use a :ref:`full-blown WSGI server `. -In order to use the interactive debuggger, debugging must be enables both on -the application and the simple server, here is the "hello world" example with debugging and -:func:`run_simple ` : +In order to use the interactive debuggger, debugging must be enabled both on +the application and the simple server, here is the "hello world" example with +debugging and :func:`run_simple `:: from flask import Flask from werkzeug.serving import run_simple @@ -45,7 +45,8 @@ the application and the simple server, here is the "hello world" example with de return 'Hello World!' if __name__ == '__main__': - run_simple('localhost', 5000, app, use_reloader=True, use_debugger=True, use_evalex=True) + run_simple('localhost', 5000, app, + use_reloader=True, use_debugger=True, use_evalex=True) Combining Applications From 91efb90fb3d580bad438c353bdfe4d604051a3a4 Mon Sep 17 00:00:00 2001 From: jfinkels Date: Sun, 1 Apr 2012 13:03:22 -0300 Subject: [PATCH 1004/3747] Removed extra blank line to fix ReST formatted documentation string in wrappers.py. Should have gone with pull request #439. --- flask/wrappers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/wrappers.py b/flask/wrappers.py index 541d26ef..3ee718ff 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -118,7 +118,6 @@ class Request(RequestBase): this server could not understand."} .. versionchanged:: 0.9 - Return a :class:`JSONBadRequest` instead of a :class:`~werkzeug.exceptions.BadRequest` by default. From f46f7155b27a741081fc13fa7fb1db53e54f5683 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 1 Apr 2012 20:57:50 -0400 Subject: [PATCH 1005/3747] 2012 --- LICENSE | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 5d269389..dc01ee1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS +Copyright (c) 2012 by Armin Ronacher and contributors. See AUTHORS for more details. Some rights reserved. diff --git a/docs/conf.py b/docs/conf.py index 16d7e670..30df7147 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2010, Armin Ronacher' +copyright = u'2012, Armin Ronacher' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From b16c988f1e7de5f0ec9dae11817b9109de59355d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 3 Apr 2012 20:55:58 -0400 Subject: [PATCH 1006/3747] Fix static endpoint name mention in quickstart. --- docs/quickstart.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8f38aff5..8497f082 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -333,8 +333,7 @@ configured to serve them for you, but during development Flask can do that as well. Just create a folder called `static` in your package or next to your module and it will be available at `/static` on the application. -To generate URLs that part of the URL, use the special ``'static'`` URL -name:: +To generate URLs for static files, use the special ``'static'`` endpoint name:: url_for('static', filename='style.css') From 492ef06bff569d6037fa43e561b203fe444b60d5 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 4 Apr 2012 11:39:07 -0400 Subject: [PATCH 1007/3747] Clarify use of context-locals with signals. --- docs/signals.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/signals.rst b/docs/signals.rst index 2d3878f7..df92dce7 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -131,6 +131,8 @@ debugging. You can access the name of the signal with the missing blinker installations, you can do so by using the :class:`flask.signals.Namespace` class. +.. _signals-sending: + Sending Signals --------------- @@ -156,6 +158,17 @@ function, you can pass ``current_app._get_current_object()`` as sender. that :data:`~flask.current_app` is a proxy and not the real application object. + +Signals and Flask's Request Context +----------------------------------- + +Signals fully support :ref:`reqcontext` when receiving signals. Context-local +variables are consistently available between :data:`~flask.request_started` and +:data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others +as needed. Note the limitations described in :ref:`signals-sending` and the +:data:`~flask.request_tearing_down` signal. + + Decorator Based Signal Subscriptions ------------------------------------ From f07199009c463ed5eaab7b2cacd785d46f87699d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 4 Apr 2012 11:41:40 -0400 Subject: [PATCH 1008/3747] Fix reqcontext ref in signals doc. --- docs/signals.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index df92dce7..a4cd4157 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -162,11 +162,11 @@ function, you can pass ``current_app._get_current_object()`` as sender. Signals and Flask's Request Context ----------------------------------- -Signals fully support :ref:`reqcontext` when receiving signals. Context-local -variables are consistently available between :data:`~flask.request_started` and -:data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others -as needed. Note the limitations described in :ref:`signals-sending` and the -:data:`~flask.request_tearing_down` signal. +Signals fully support :ref:`request-context` when receiving signals. +Context-local variables are consistently available between +:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can +rely on :class:`flask.g` and others as needed. Note the limitations described +in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. Decorator Based Signal Subscriptions From 9c48387072128c32dde06dc9a6e812195f18012d Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Sun, 8 Apr 2012 18:09:12 -0500 Subject: [PATCH 1009/3747] Removed unneeded print statements form mongokit pattern doc --- docs/patterns/mongokit.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index b50cf456..b4b6fc01 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -122,9 +122,6 @@ collection first, this is somewhat the same as a table in the SQL world. >>> user = {'name': u'admin', 'email': u'admin@localhost'} >>> collection.insert(user) -print list(collection.find()) -print collection.find_one({'name': u'admin'}) - MongoKit will automatically commit for us. To query your database, you use the collection directly: From a1305973bfef18a341224ad4f748c35d77a64cdb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 14:19:13 +0100 Subject: [PATCH 1010/3747] Fixed a typo in a comment --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 47ac0cc1..f9558d2e 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -95,7 +95,7 @@ class RequestContext(object): self.match_request() - # XXX: Support for deprecated functionality. This is doing away with + # XXX: Support for deprecated functionality. This is going away with # Flask 1.0 blueprint = self.request.blueprint if blueprint is not None: From 47288231fe8f9c6b2c413d50160c32c3884d5785 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 14:34:12 +0100 Subject: [PATCH 1011/3747] Implemented a separate application context. --- flask/app.py | 17 ++++++++++++++- flask/ctx.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++- flask/globals.py | 10 ++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/flask/app.py b/flask/app.py index 15e432de..38d31f95 100644 --- a/flask/app.py +++ b/flask/app.py @@ -28,7 +28,7 @@ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ find_package from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import RequestContext +from .ctx import RequestContext, AppContext from .globals import _request_ctx_stack, request from .sessions import SecureCookieSessionInterface from .module import blueprint_is_module @@ -1458,6 +1458,21 @@ class Flask(_PackageBoundObject): return rv request_tearing_down.send(self) + def app_context(self): + """Binds the application only. For as long as the application is bound + to the current context the :data:`flask.current_app` points to that + application. An application context is automatically created when a + request context is pushed if necessary. + + Example usage:: + + with app.app_context(): + ... + + .. versionadded:: 0.9 + """ + return AppContext(self) + def request_context(self, environ): """Creates a :class:`~flask.ctx.RequestContext` from the given environment and binds it to the current context. This must be used in diff --git a/flask/ctx.py b/flask/ctx.py index f9558d2e..7bfd598e 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -11,7 +11,7 @@ from werkzeug.exceptions import HTTPException -from .globals import _request_ctx_stack +from .globals import _request_ctx_stack, _app_ctx_stack from .module import blueprint_is_module @@ -19,6 +19,14 @@ class _RequestGlobals(object): pass +def _push_app_if_necessary(app): + top = _app_ctx_stack.top + if top is None or top.app != app: + ctx = app.app_context() + ctx.push() + return ctx + + def has_request_context(): """If you have code that wants to test if a request context is there or not this function can be used. For instance, you may want to take advantage @@ -51,6 +59,36 @@ def has_request_context(): return _request_ctx_stack.top is not None +class AppContext(object): + """The application context binds an application object implicitly + to the current thread or greenlet, similar to how the + :class:`RequestContext` binds request information. The application + context is also implicitly created if a request context is created + but the application is not on top of the individual application + context. + """ + + def __init__(self, app): + self.app = app + + def push(self): + """Binds the app context to the current context.""" + _app_ctx_stack.push(self) + + def pop(self): + """Pops the app context.""" + rv = _app_ctx_stack.pop() + assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ + % (rv, self) + + def __enter__(self): + self.push() + return self + + def __exit__(self, exc_type, exc_value, tb): + self.pop() + + class RequestContext(object): """The request context contains all request relevant information. It is created at the beginning of the request and pushed to the @@ -93,6 +131,11 @@ class RequestContext(object): # is pushed the preserved context is popped. self.preserved = False + # Indicates if pushing this request context also triggered the pushing + # of an application context. If it implicitly pushed an application + # context, it will be stored there + self._pushed_application_context = None + self.match_request() # XXX: Support for deprecated functionality. This is going away with @@ -130,6 +173,10 @@ class RequestContext(object): if top is not None and top.preserved: top.pop() + # Before we push the request context we have to ensure that there + # is an application context. + self._pushed_application_context = _push_app_if_necessary(self.app) + _request_ctx_stack.push(self) # Open the session at the moment that the request context is @@ -154,6 +201,11 @@ class RequestContext(object): # so that we don't require the GC to be active. rv.request.environ['werkzeug.request'] = None + # Get rid of the app as well if necessary. + if self._pushed_application_context: + self._pushed_application_context.pop() + self._pushed_application_context = None + def __enter__(self): self.push() return self diff --git a/flask/globals.py b/flask/globals.py index 16580d16..f6d62485 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -20,9 +20,17 @@ def _lookup_object(name): return getattr(top, name) +def _find_app(): + top = _app_ctx_stack.top + if top is None: + raise RuntimeError('working outside of application context') + return top.app + + # context locals _request_ctx_stack = LocalStack() -current_app = LocalProxy(partial(_lookup_object, 'app')) +_app_ctx_stack = LocalStack() +current_app = LocalProxy(_find_app) request = LocalProxy(partial(_lookup_object, 'request')) session = LocalProxy(partial(_lookup_object, 'session')) g = LocalProxy(partial(_lookup_object, 'g')) From 307d1bc4e51fc35282f2d504b9df3359604b8f8a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 15:04:35 +0100 Subject: [PATCH 1012/3747] Added support for basic URL generation without request contexts. --- flask/app.py | 19 ++++++++++++--- flask/ctx.py | 1 + flask/helpers.py | 50 +++++++++++++++++++++++++-------------- flask/testsuite/appctx.py | 38 +++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 flask/testsuite/appctx.py diff --git a/flask/app.py b/flask/app.py index 38d31f95..215bf0ad 100644 --- a/flask/app.py +++ b/flask/app.py @@ -250,7 +250,8 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, - 'TRAP_HTTP_EXCEPTIONS': False + 'TRAP_HTTP_EXCEPTIONS': False, + 'PREFERRED_URL_SCHEME': 'http' }) #: The rule object to use for URL rules created. This is used by @@ -1370,9 +1371,21 @@ class Flask(_PackageBoundObject): so the request is passed explicitly. .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + UR adapter is created for the application context. """ - return self.url_map.bind_to_environ(request.environ, - server_name=self.config['SERVER_NAME']) + if request is not None: + return self.url_map.bind_to_environ(request.environ, + server_name=self.config['SERVER_NAME']) + # We need at the very least the server name to be set for this + # to work. + if self.config['SERVER_NAME'] is not None: + return self.url_map.bind( + self.config['SERVER_NAME'], + script_name=self.config['APPLICATION_ROOT'] or '/', + url_scheme=self.config['PREFERRED_URL_SCHEME']) def inject_url_defaults(self, endpoint, values): """Injects the URL defaults for the given endpoint directly into diff --git a/flask/ctx.py b/flask/ctx.py index 7bfd598e..a9088cf4 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -70,6 +70,7 @@ class AppContext(object): def __init__(self, app): self.app = app + self.url_adapter = app.create_url_adapter(None) def push(self): """Binds the app context to the current context.""" diff --git a/flask/helpers.py b/flask/helpers.py index 25250d26..26be5e30 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -50,7 +50,8 @@ except ImportError: from jinja2 import FileSystemLoader -from .globals import session, _request_ctx_stack, current_app, request +from .globals import session, _request_ctx_stack, _app_ctx_stack, \ + current_app, request def _assert_have_json(): @@ -197,27 +198,40 @@ def url_for(endpoint, **values): :param _anchor: if provided this is added as anchor to the URL. :param _method: if provided this explicitly specifies an HTTP method. """ - ctx = _request_ctx_stack.top - blueprint_name = request.blueprint - if not ctx.request._is_old_module: - if endpoint[:1] == '.': - if blueprint_name is not None: - endpoint = blueprint_name + endpoint - else: + appctx = _app_ctx_stack.top + reqctx = _request_ctx_stack.top + + # If request specific information is available we have some extra + # features that support "relative" urls. + if reqctx is not None: + url_adapter = reqctx.url_adapter + blueprint_name = request.blueprint + if not reqctx.request._is_old_module: + if endpoint[:1] == '.': + if blueprint_name is not None: + endpoint = blueprint_name + endpoint + else: + endpoint = endpoint[1:] + else: + # TODO: get rid of this deprecated functionality in 1.0 + if '.' not in endpoint: + if blueprint_name is not None: + endpoint = blueprint_name + '.' + endpoint + elif endpoint.startswith('.'): endpoint = endpoint[1:] + external = values.pop('_external', False) + + # Otherwise go with the url adapter from the appctx and make + # the urls external by default. else: - # TODO: get rid of this deprecated functionality in 1.0 - if '.' not in endpoint: - if blueprint_name is not None: - endpoint = blueprint_name + '.' + endpoint - elif endpoint.startswith('.'): - endpoint = endpoint[1:] - external = values.pop('_external', False) + url_adapter = appctx.url_adapter + external = values.pop('_external', True) + anchor = values.pop('_anchor', None) method = values.pop('_method', None) - ctx.app.inject_url_defaults(endpoint, values) - rv = ctx.url_adapter.build(endpoint, values, method=method, - force_external=external) + appctx.app.inject_url_defaults(endpoint, values) + rv = url_adapter.build(endpoint, values, method=method, + force_external=external) if anchor is not None: rv += '#' + url_quote(anchor) return rv diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py new file mode 100644 index 00000000..2c198047 --- /dev/null +++ b/flask/testsuite/appctx.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.appctx + ~~~~~~~~~~~~~~~~~~~~~~ + + Tests the application context. + + :copyright: (c) 2012 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +from flask.testsuite import FlaskTestCase + + +class AppContextTestCase(FlaskTestCase): + + def test_basic_support(self): + app = flask.Flask(__name__) + app.config['SERVER_NAME'] = 'localhost' + app.config['PREFERRED_URL_SCHEME'] = 'https' + + @app.route('/') + def index(): + pass + + with app.app_context(): + rv = flask.url_for('index') + self.assert_equal(rv, 'https://localhost/') + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(AppContextTestCase)) + return suite From f8f2e2dff481e55fa9ae7cc3f70f36d61bcf56d7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 15:16:09 +0100 Subject: [PATCH 1013/3747] Added more tests for the new stack behavior. --- flask/__init__.py | 3 ++- flask/helpers.py | 8 ++++++++ flask/testsuite/appctx.py | 24 +++++++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 54bfedda..f35ef328 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -23,7 +23,8 @@ from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ get_template_attribute, make_response, safe_join -from .globals import current_app, g, request, session, _request_ctx_stack +from .globals import current_app, g, request, session, _request_ctx_stack, \ + _app_ctx_stack from .ctx import has_request_context from .module import Module from .blueprints import Blueprint diff --git a/flask/helpers.py b/flask/helpers.py index 26be5e30..b86ce158 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -200,6 +200,9 @@ def url_for(endpoint, **values): """ appctx = _app_ctx_stack.top reqctx = _request_ctx_stack.top + if appctx is None: + raise RuntimeError('Attempted to generate a URL with the application ' + 'context being pushed. This has to be executed ') # If request specific information is available we have some extra # features that support "relative" urls. @@ -225,6 +228,11 @@ def url_for(endpoint, **values): # the urls external by default. else: url_adapter = appctx.url_adapter + if url_adapter is None: + raise RuntimeError('Application was not able to create a URL ' + 'adapter for request independent URL generation. ' + 'You might be able to fix this by setting ' + 'the SERVER_NAME config variable.') external = values.pop('_external', True) anchor = values.pop('_anchor', None) diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index 2c198047..c60dbc67 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -18,7 +18,7 @@ from flask.testsuite import FlaskTestCase class AppContextTestCase(FlaskTestCase): - def test_basic_support(self): + def test_basic_url_generation(self): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'localhost' app.config['PREFERRED_URL_SCHEME'] = 'https' @@ -31,6 +31,28 @@ class AppContextTestCase(FlaskTestCase): rv = flask.url_for('index') self.assert_equal(rv, 'https://localhost/') + def test_url_generation_requires_server_name(self): + app = flask.Flask(__name__) + with app.app_context(): + with self.assert_raises(RuntimeError): + flask.url_for('index') + + def test_url_generation_without_context_fails(self): + with self.assert_raises(RuntimeError): + flask.url_for('index') + + def test_request_context_means_app_context(self): + app = flask.Flask(__name__) + with app.test_request_context(): + self.assert_equal(flask.current_app._get_current_object(), app) + self.assert_equal(flask._app_ctx_stack.top, None) + + def test_app_context_provides_current_app(self): + app = flask.Flask(__name__) + with app.app_context(): + self.assert_equal(flask.current_app._get_current_object(), app) + self.assert_equal(flask._app_ctx_stack.top, None) + def suite(): suite = unittest.TestSuite() From 0207e90155abe937568727e4e9eca949b8247cd5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 15:22:36 +0100 Subject: [PATCH 1014/3747] Updated docs for the app context. --- CHANGES | 3 +++ docs/api.rst | 16 +++++++++++++++- flask/ctx.py | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 8311e2e7..5436a857 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,9 @@ Relase date to be decided, codename to be chosen. - The :meth:`flask.render_template` method now accepts a either an iterable of template names or a single template name. Previously, it only accepted a single template name. On an iterable, the first template found is rendered. +- Added :meth:`flask.Flask.app_context` which works very similar to the + request context but only provides access to the current application. This + also adds support for URL generation without an active request context. Version 0.8.1 diff --git a/docs/api.rst b/docs/api.rst index ec7e4f63..78ed2d8d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -265,12 +265,16 @@ Useful Functions and Classes Points to the application handling the request. This is useful for extensions that want to support multiple applications running side - by side. + by side. This is powered by the application context and not by the + request context, so you can change the value of this proxy by + using the :meth:`~flask.Flask.app_context` method. This is a proxy. See :ref:`notes-on-proxies` for more information. .. autofunction:: has_request_context +.. autofunction:: has_app_context + .. autofunction:: url_for .. function:: abort(code) @@ -412,6 +416,16 @@ Useful Internals if ctx is not None: return ctx.session +.. autoclass:: flask.ctx.AppContext + :members: + +.. data:: _app_ctx_stack + + Works similar to the request context but only binds the application. + This is mainly there for extensions to store data. + + .. versionadded:: 0.9 + .. autoclass:: flask.blueprints.BlueprintSetupState :members: diff --git a/flask/ctx.py b/flask/ctx.py index a9088cf4..887b2598 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -59,6 +59,16 @@ def has_request_context(): return _request_ctx_stack.top is not None +def has_app_context(): + """Worksl ike :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _app_ctx_stack.top is not None + + class AppContext(object): """The application context binds an application object implicitly to the current thread or greenlet, similar to how the From ab110d8fe573c92f0f607a05cf31549399b98c1a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 15:24:43 +0100 Subject: [PATCH 1015/3747] Documented config changes --- docs/config.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/config.rst b/docs/config.rst index 2f9d8307..a5d2a9f0 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -95,7 +95,10 @@ The following configuration values are used internally by Flask: ``'myapp.dev:5000'``) Note that localhost does not support subdomains so setting this to “localhost” does not - help. + help. Setting a ``SERVER_NAME`` also + by default enables URL generation + without a request context but with an + application context. ``APPLICATION_ROOT`` If the application does not occupy a whole domain or subdomain this can be set to the path where the application @@ -126,6 +129,9 @@ The following configuration values are used internally by Flask: used to debug those situations. If this config is set to ``True`` you will get a regular traceback instead. +``PREFERRED_URL_SCHEME`` The URL scheme that should be used for + URL generation if no URL scheme is + available. This defaults to ``http``. ================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -165,6 +171,9 @@ The following configuration values are used internally by Flask: ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``, ``SESSION_COOKIE_SECURE`` +.. versionadded:: 0.9 + ``PREFERRED_URL_SCHEME`` + Configuring from Files ---------------------- From cf1641e5beec1ec11f418fa6e775fc44b9410180 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 15:56:33 +0100 Subject: [PATCH 1016/3747] Changed the implementation of returning tuples from functions --- CHANGES | 2 ++ docs/quickstart.rst | 7 +++-- docs/upgrading.rst | 15 +++++++++++ flask/app.py | 55 ++++++++++++++++++++++++---------------- flask/testsuite/basic.py | 15 +++++------ 5 files changed, 61 insertions(+), 33 deletions(-) diff --git a/CHANGES b/CHANGES index bb03d088..cb7b751e 100644 --- a/CHANGES +++ b/CHANGES @@ -63,6 +63,8 @@ Relase date to be decided, codename to be chosen. the `get_send_file_options` hook is used. - Fixed an assumption in sessions implementation which could break message flashing on sessions implementations which use external storage. +- Changed the behavior of tuple return values from functions. They are no + longer arguments to the response object, they now have a defined meaning. Version 0.8.1 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8497f082..f7b6ee02 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -674,8 +674,11 @@ converting return values into response objects is as follows: returned from the view. 2. If it's a string, a response object is created with that data and the default parameters. -3. If a tuple is returned the response object is created by passing the - tuple as arguments to the response object's constructor. +3. If a tuple is returned the items in the tuple can provide extra + information. Such tuples have to be in the form ``(response, status, + headers)`` where at least one item has to be in the tuple. The + `status` value will override the status code and `headers` can be a + list or dictionary of additional header values. 4. If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 0ba46c13..ab00624e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,21 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +Version 0.9 +----------- + +The behavior of returning tuples from a function was simplified. If you +return a tuple it no longer defines the arguments for the response object +you're creating, it's now always a tuple in the form ``(response, status, +headers)`` where at least one item has to be provided. If you depend on +the old behavior, you can add it easily by subclassing Flask:: + + class TraditionalFlask(Flask): + def make_response(self, rv): + if isinstance(rv, tuple): + return self.response_class(*rv) + return Flask.make_response(self, rv) + Version 0.8 ----------- diff --git a/flask/app.py b/flask/app.py index e16b35bf..a0ffed66 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1354,37 +1354,48 @@ class Flask(_PackageBoundObject): string as body :class:`unicode` a response object is created with the string encoded to utf-8 as body - :class:`tuple` the response object is created with the - contents of the tuple as arguments a WSGI function the function is called as WSGI application and buffered as response object + :class:`tuple` A tuple in the form ``(response, status, + headers)`` where `response` is any of the + types defined here, `status` is a string + or an integer and `headers` is a list of + a dictionary with header values. ======================= =========================================== :param rv: the return value from the view function + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. """ + status = headers = None + if isinstance(rv, tuple): + rv, status, headers = rv + (None,) * (3 - len(rv)) + if rv is None: raise ValueError('View function did not return a response') - if isinstance(rv, self.response_class): - return rv - if isinstance(rv, basestring): - return self.response_class(rv) - if isinstance(rv, tuple): - if len(rv) > 0 and isinstance(rv[0], self.response_class): - original = rv[0] - new_response = self.response_class('', *rv[1:]) - if len(rv) < 3: - # The args for the response class are - # response=None, status=None, headers=None, - # mimetype=None, content_type=None, ... - # so if there's at least 3 elements the rv - # tuple contains header information so the - # headers from rv[0] "win." - new_response.headers = original.headers - new_response.response = original.response - return new_response + + if not isinstance(rv, self.response_class): + # When we create a response object directly, we let the constructor + # set the headers and status. We do this because there can be + # some extra logic involved when creating these objects with + # specific values (like defualt content type selection). + if isinstance(rv, basestring): + rv = self.response_class(rv, headers=headers, status=status) + headers = status = None else: - return self.response_class(*rv) - return self.response_class.force_type(rv, request.environ) + rv = self.response_class.force_type(rv, request.environ) + + if status is not None: + if isinstance(status, basestring): + rv.status = status + else: + rv.status_code = status + if headers: + rv.headers.extend(headers) + + return rv def create_url_adapter(self, request): """Creates a URL adapter for the given request. The URL adapter diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 41efb196..0a4b1d9c 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -631,7 +631,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): return u'Hällo Wörld'.encode('utf-8') @app.route('/args') def from_tuple(): - return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain' + return 'Meh', 400, { + 'X-Foo': 'Testing', + 'Content-Type': 'text/plain; charset=utf-8' + } c = app.test_client() self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) @@ -677,16 +680,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = flask.make_response( flask.Response('', headers={'Content-Type': 'text/html'}), - 400, None, 'application/json') - self.assertEqual(rv.status_code, 400) - self.assertEqual(rv.headers['Content-Type'], 'application/json') - - rv = flask.make_response( - flask.Response('', mimetype='application/json'), - 400, {'Content-Type': 'text/html'}) + 400, [('X-Foo', 'bar')]) self.assertEqual(rv.status_code, 400) self.assertEqual(rv.headers['Content-Type'], 'text/html') - + self.assertEqual(rv.headers['X-Foo'], 'bar') def test_url_generation(self): app = flask.Flask(__name__) From 34bbd3100bbfb9a406a417a9fe4e8944aa87c629 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 16:11:35 +0100 Subject: [PATCH 1017/3747] Fixed a failing testcase --- flask/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 01f4081e..1be2daf7 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -726,7 +726,9 @@ class _PackageBoundObject(object): .. versionadded:: 0.9 """ - return {} + options = {} + options['cache_timeout'] = current_app.config['SEND_FILE_MAX_AGE_DEFAULT'] + return options def send_static_file(self, filename): """Function used internally to send static files from the static From 9bed20c07c40a163077b936f740a4e96a6213688 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:29:00 +0100 Subject: [PATCH 1018/3747] Added documentation for appcontext and teardown handlers --- docs/api.rst | 14 +++++- docs/appcontext.rst | 88 +++++++++++++++++++++++++++++++++ docs/contents.rst.inc | 1 + docs/extensiondev.rst | 100 ++++++++++++++++++++++++++------------ docs/reqcontext.rst | 26 ++-------- docs/signals.rst | 23 +++++++++ flask/app.py | 68 +++++++++++++++++++++++--- flask/ctx.py | 18 +++++-- flask/signals.py | 1 + flask/testsuite/appctx.py | 12 +++++ 10 files changed, 287 insertions(+), 64 deletions(-) create mode 100644 docs/appcontext.rst diff --git a/docs/api.rst b/docs/api.rst index 97332870..b09bcad5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -469,8 +469,18 @@ Signals .. data:: request_tearing_down This signal is sent when the application is tearing down the request. - This is always called, even if an error happened. No arguments are - provided. + This is always called, even if an error happened. An `exc` keyword + argument is passed with the exception that caused the teardown. + + .. versionchanged:: 0.9 + The `exc` parameter was added. + +.. data:: appcontext_tearing_down + + This signal is sent when the application is tearing down the + application context. This is always called, even if an error happened. + An `exc` keyword argument is passed with the exception that caused the + teardown. .. currentmodule:: None diff --git a/docs/appcontext.rst b/docs/appcontext.rst new file mode 100644 index 00000000..c331ffa5 --- /dev/null +++ b/docs/appcontext.rst @@ -0,0 +1,88 @@ +.. _app_context: + +The Application Context +======================= + +.. versionadded:: 0.9 + +One of the design ideas behind Flask is that there are two different +“states” in which code is executed. The application setup state in which +the application implicitly is on the module level. It starts when the +:class:`Flask` object is instantiated, and it implicitly ends when the +first request comes in. While the application is in this state a few +assumptions are true: + +- the programmer can modify the application object safely. +- no request handling happened so far +- you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +On the contrast, during request handling, a couple of other rules exist: + +- while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. +- any code can get hold of these objects at any time. + +There is a third state which is sitting in between a little bit. +Sometimes you are dealing with an application in a way that is similar to +how you interact with applications during request handling just that there +is no request active. Consider for instance that you're sitting in an +interactive Python shell and interacting with the application, or a +command line application. + +The application context is what powers the :data:`~flask.current_app` +context local. + +Purpose of the Application Context +---------------------------------- + +The main reason for the application's context existance is that in the +past a bunch of functionality was attached to the request context in lack +of a better solution. Since one of the pillar's of Flask's design is that +you can have more than one application in the same Python process. + +So how does the code find the “right” application? In the past we +recommended passing applications around explicitly, but that caused issues +with libraries that were not designed with that in mind for libraries for +which it was too inconvenient to make this work. + +A common workaround for that problem was to use the +:data:`~flask.current_app` proxy later on, which was bound to the current +request's application reference. Since however creating such a request +context is an unnecessarily expensive operation in case there is no +request around, the application context was introduced. + +Creating an Application Context +------------------------------- + +To make an application context there are two ways. The first one is the +implicit one: whenever a request context is pushed, an application context +will be created alongside if this is necessary. As a result of that, you +can ignore the existance of the application context unless you need it. + +The second way is the explicit way using the +:meth:`~flask.Flask.app_context` method:: + + from flask import Flask, current_app + + app = Flask(__name__) + with app.app_context(): + # within this block, current_app points to app. + print current_app.name + +The application context is also used by the :func:`~flask.url_for` +function in case a ``SERVER_NAME`` was configured. This allows you to +generate URLs even in the absence of a request. + +Locality of the Context +----------------------- + +The application context is created and destroyed as necessary. It never +moves between threads and it will not be shared between requests. As such +it is the perfect place to store database connection information and other +things. The internal stack object is called :data:`flask._app_ctx_stack`. +Extensions are free to store additional information on the topmost level, +assuming they pick a sufficiently unique name. + +For more information about that, see :ref:`extension-dev`. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index a8ebc0d7..a1893c48 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -18,6 +18,7 @@ instructions for web development with Flask. config signals views + appcontext reqcontext blueprints extensions diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 60aa6c37..2511cec7 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -1,3 +1,5 @@ +.. _extension-dev: + Flask Extension Development =========================== @@ -152,6 +154,11 @@ What to use depends on what you have in mind. For the SQLite 3 extension we will use the class-based approach because it will provide users with an object that handles opening and closing database connections. +What's important about classes is that they encourage to be shared around +on module level. In that case, the object itself must not under any +circumstances store any application specific state and must be shareable +between different application. + The Extension Code ------------------ @@ -159,7 +166,13 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: import sqlite3 - from flask import _request_ctx_stack + # Find the stack on which we want to store the database connection. + # Starting with Flask 0.9, the _app_ctx_stack is the correct one, + # before that we need to use the _request_ctx_stack. + try: + from flask import _app_ctx_stack as stack + except ImportError: + from flask import _request_ctx_stack as stack class SQLite3(object): @@ -172,26 +185,28 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: self.app = None def init_app(self, app): - self.app = app - self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.teardown_request(self.teardown_request) - self.app.before_request(self.before_request) + app.config.setdefault('SQLITE3_DATABASE', ':memory:') + # Use the newstyle teardown_appcontext if it's available, + # otherwise fall back to the request context + if hasattr(app, 'teardown_appcontext'): + app.teardown_appcontext(self.teardown) + else: + app.teardown_request(self.teardown) def connect(self): return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) - def before_request(self): - ctx = _request_ctx_stack.top - ctx.sqlite3_db = self.connect() - - def teardown_request(self, exception): - ctx = _request_ctx_stack.top - ctx.sqlite3_db.close() + def teardown(self, exception): + ctx = stack.top + if hasattr(ctx, 'sqlite3_db'): + ctx.sqlite3_db.close() @property def connection(self): - ctx = _request_ctx_stack.top + ctx = stack.top if ctx is not None: + if not hasattr(ctx, 'sqlite3_db'): + ctx.sqlite3_db = self.connect() return ctx.sqlite3_db @@ -204,14 +219,19 @@ So here's what these lines of code do: factory pattern for creating applications. The ``init_app`` will set the configuration for the database, defaulting to an in memory database if no configuration is supplied. In addition, the ``init_app`` method attaches - ``before_request`` and ``teardown_request`` handlers. + the ``teardown`` handler. It will try to use the newstyle app context + handler and if it does not exist, falls back to the request context + one. 3. Next, we define a ``connect`` method that opens a database connection. -4. Then we set up the request handlers we bound to the app above. Note here - that we're attaching our database connection to the top request context via - ``_request_ctx_stack.top``. Extensions should use the top context and not the - ``g`` object to store things like database connections. -5. Finally, we add a ``connection`` property that simplifies access to the context's - database. +4. Finally, we add a ``connection`` property that on first access opens + the database connection and stores it on the context. + + Note here that we're attaching our database connection to the top + application context via ``_app_ctx_stack.top``. Extensions should use + the top context for storing their own information with a sufficiently + complex name. Note that we're falling back to the + ``_request_ctx_stack.top`` if the application is using an older + version of Flask that does not support it. So why did we decide on a class-based approach here? Because using our extension looks something like this:: @@ -241,19 +261,38 @@ for creating apps:: Keep in mind that supporting this factory pattern for creating apps is required for approved flask extensions (described below). +.. admonition:: Note on ``init_app`` -Using _request_ctx_stack ------------------------- + As you noticed, ``init_app`` does not assign ``app`` to ``self``. This + is intentional! Class based Flask extensions must only store the + application on the object when the application was passed to the + constructor. This tells the extension: I am not interested in using + multiple applications. -In the example above, before every request, a ``sqlite3_db`` variable is assigned -to ``_request_ctx_stack.top``. In a view function, this variable is accessible -using the ``connection`` property of ``SQLite3``. During the teardown of a -request, the ``sqlite3_db`` connection is closed. By using this pattern, the -*same* connection to the sqlite3 database is accessible to anything that needs it -for the duration of the request. + When the extension needs to find the current application and it does + not have a reference to it, it must either use the + :data:`~flask.current_app` context local or change the API in a way + that you can pass the application explicitly. -End-Of-Request Behavior ------------------------ + +Using _app_ctx_stack +-------------------- + +In the example above, before every request, a ``sqlite3_db`` variable is +assigned to ``_app_ctx_stack.top``. In a view function, this variable is +accessible using the ``connection`` property of ``SQLite3``. During the +teardown of a request, the ``sqlite3_db`` connection is closed. By using +this pattern, the *same* connection to the sqlite3 database is accessible +to anything that needs it for the duration of the request. + +If the :data:`~flask._app_ctx_stack` does not exist because the user uses +an old version of Flask, it is recommended to fall back to +:data:`~flask._request_ctx_stack` which is bound to a request. + +Teardown Behavior +----------------- + +*This is only relevant if you want to support Flask 0.6 and older* Due to the change in Flask 0.7 regarding functions that are run at the end of the request your extension will have to be extra careful there if it @@ -270,7 +309,6 @@ pattern is a good way to support both:: else: app.after_request(close_connection) - Strictly speaking the above code is wrong, because teardown functions are passed the exception and typically don't return anything. However because the return value is discarded this will just work assuming that the code diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 0249b88e..327afe6c 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -6,27 +6,7 @@ The Request Context This document describes the behavior in Flask 0.7 which is mostly in line with the old behavior but has some small, subtle differences. -One of the design ideas behind Flask is that there are two different -“states” in which code is executed. The application setup state in which -the application implicitly is on the module level. It starts when the -:class:`Flask` object is instantiated, and it implicitly ends when the -first request comes in. While the application is in this state a few -assumptions are true: - -- the programmer can modify the application object safely. -- no request handling happened so far -- you have to have a reference to the application object in order to - modify it, there is no magic proxy that can give you a reference to - the application object you're currently creating or modifying. - -On the contrast, during request handling, a couple of other rules exist: - -- while a request is active, the context local objects - (:data:`flask.request` and others) point to the current request. -- any code can get hold of these objects at any time. - -The magic that makes this works is internally referred in Flask as the -“request context”. +It is recommended that you read the :api:`app-context` chapter first. Diving into Context Locals -------------------------- @@ -107,6 +87,10 @@ the very top, :meth:`~flask.ctx.RequestContext.pop` removes it from the stack again. On popping the application's :func:`~flask.Flask.teardown_request` functions are also executed. +Another thing of note is that the request context will automatically also +create an :ref:`application context ` when it's pushed and +there is no application context for that application so far. + .. _callbacks-and-errors: Callbacks and Errors diff --git a/docs/signals.rst b/docs/signals.rst index a4cd4157..959c53bd 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -268,4 +268,27 @@ The following signals exist in Flask: from flask import request_tearing_down request_tearing_down.connect(close_db_connection, app) + As of Flask 0.9, this will also be passed an `exc` keyword argument + that has a reference to the exception that caused the teardown if + there was one. + +.. data:: flask.appcontext_tearing_down + :noindex: + + This signal is sent when the app context is tearing down. This is always + called, even if an exception is caused. Currently functions listening + to this signal are called after the regular teardown handlers, but this + is not something you can rely on. + + Example subscriber:: + + def close_db_connection(sender, **extra): + session.close() + + from flask import request_tearing_down + appcontext_tearing_down.connect(close_db_connection, app) + + This will also be passed an `exc` keyword argument that has a reference + to the exception that caused the teardown if there was one. + .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/flask/app.py b/flask/app.py index a0ffed66..8460f476 100644 --- a/flask/app.py +++ b/flask/app.py @@ -35,7 +35,7 @@ from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ - request_tearing_down + request_tearing_down, appcontext_tearing_down # a lock used for logger initialization _logger_lock = Lock() @@ -364,6 +364,14 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 self.teardown_request_funcs = {} + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs = [] + #: A dictionary with lists of functions that can be used as URL #: value processor functions. Whenever a URL is built these functions #: are called to modify the dictionary of values in place. The key @@ -1106,10 +1114,42 @@ class Flask(_PackageBoundObject): that they will fail. If they do execute code that might fail they will have to surround the execution of these code by try/except statements and log ocurring errors. + + When a teardown function was called because of a exception it will + be passed an error object. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f + @setupmethod + def teardown_appcontext(self, f): + """Registers a function to be called when the application context + ends. These functions are typically also called when the request + context is popped. + + Example:: + + ctx = app.app_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the app context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Since a request context typically also manages an application + context it would also be called when you pop a request context. + + When a teardown function was called because of an exception it will + be passed an error object. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + @setupmethod def context_processor(self, f): """Registers a template context processor function.""" @@ -1485,23 +1525,39 @@ class Flask(_PackageBoundObject): self.save_session(ctx.session, response) return response - def do_teardown_request(self): + def do_teardown_request(self, exc=None): """Called after the actual request dispatching and will call every as :meth:`teardown_request` decorated function. This is not actually called by the :class:`Flask` object itself but is always triggered when the request context is popped. That way we have a tighter control over certain resources under testing environments. + + .. versionchanged:: 0.9 + Added the `exc` argument. Previously this was always using the + current exception information. """ + if exc is None: + exc = sys.exc_info()[1] funcs = reversed(self.teardown_request_funcs.get(None, ())) bp = _request_ctx_stack.top.request.blueprint if bp is not None and bp in self.teardown_request_funcs: funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) - exc = sys.exc_info()[1] for func in funcs: rv = func(exc) - if rv is not None: - return rv - request_tearing_down.send(self) + request_tearing_down.send(self, exc=exc) + + def do_teardown_appcontext(self, exc=None): + """Called when an application context is popped. This works pretty + much the same as :meth:`do_teardown_request` but for the application + context. + + .. versionadded:: 0.9 + """ + if exc is None: + exc = sys.exc_info()[1] + for func in reversed(self.teardown_appcontext_funcs): + func(exc) + appcontext_tearing_down.send(self, exc=exc) def app_context(self): """Binds the application only. For as long as the application is bound diff --git a/flask/ctx.py b/flask/ctx.py index 887b2598..0ed5ea43 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +import sys + from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack, _app_ctx_stack @@ -86,8 +88,11 @@ class AppContext(object): """Binds the app context to the current context.""" _app_ctx_stack.push(self) - def pop(self): + def pop(self, exc=None): """Pops the app context.""" + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) rv = _app_ctx_stack.pop() assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ % (rv, self) @@ -197,13 +202,18 @@ class RequestContext(object): if self.session is None: self.session = self.app.make_null_session() - def pop(self): + def pop(self, exc=None): """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. """ self.preserved = False - self.app.do_teardown_request() + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) rv = _request_ctx_stack.pop() assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ % (rv, self) @@ -231,7 +241,7 @@ class RequestContext(object): (tb is not None and self.app.preserve_context_on_exception): self.preserved = True else: - self.pop() + self.pop(exc_value) def __repr__(self): return '<%s \'%s\' [%s] of %s>' % ( diff --git a/flask/signals.py b/flask/signals.py index eeb763d4..78a77bd5 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -49,3 +49,4 @@ request_started = _signals.signal('request-started') request_finished = _signals.signal('request-finished') request_tearing_down = _signals.signal('request-tearing-down') got_request_exception = _signals.signal('got-request-exception') +appcontext_tearing_down = _signals.signal('appcontext-tearing-down') diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index c60dbc67..a4ad479b 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -53,6 +53,18 @@ class AppContextTestCase(FlaskTestCase): self.assert_equal(flask.current_app._get_current_object(), app) self.assert_equal(flask._app_ctx_stack.top, None) + def test_app_tearing_down(self): + cleanup_stuff = [] + app = flask.Flask(__name__) + @app.teardown_appcontext + def cleanup(exception): + cleanup_stuff.append(exception) + + with app.app_context(): + pass + + self.assert_equal(cleanup_stuff, [None]) + def suite(): suite = unittest.TestSuite() From cb54c462b809e36d12af571fa36affb4af3f7e96 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:32:37 +0100 Subject: [PATCH 1019/3747] Pass exc explicitly to the inner context. --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 0ed5ea43..413ca884 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -224,7 +224,7 @@ class RequestContext(object): # Get rid of the app as well if necessary. if self._pushed_application_context: - self._pushed_application_context.pop() + self._pushed_application_context.pop(exc) self._pushed_application_context = None def __enter__(self): From 32f845ea75ce57429aa383463be6654b2af06983 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:33:14 +0100 Subject: [PATCH 1020/3747] Added an example for using the db connection without the request --- docs/extensiondev.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 2511cec7..16a354e6 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -250,6 +250,17 @@ You can then use the database from views like this:: cur = db.connection.cursor() cur.execute(...) +Likewise if you are outside of a request but you are using Flask 0.9 or +later with the app context support, you can use the database in the same +way:: + + with app.app_context(): + cur = db.connection.cursor() + cur.execute(...) + +At the end of the `with` block the teardown handles will be executed +automatically. + Additionally, the ``init_app`` method is used to support the factory pattern for creating apps:: From 52f9cefbcd6df23f75bf93804a9e84c038536fe8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:35:16 +0100 Subject: [PATCH 1021/3747] More documentation updates for 0.9 --- docs/extensiondev.rst | 4 +++- docs/upgrading.rst | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 16a354e6..86c7c721 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -224,7 +224,9 @@ So here's what these lines of code do: one. 3. Next, we define a ``connect`` method that opens a database connection. 4. Finally, we add a ``connection`` property that on first access opens - the database connection and stores it on the context. + the database connection and stores it on the context. This is also + the recommended way to handling resources: fetch resources lazily the + first time they are used. Note here that we're attaching our database connection to the top application context via ``_app_ctx_stack.top``. Extensions should use diff --git a/docs/upgrading.rst b/docs/upgrading.rst index ab00624e..5955e552 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -34,6 +34,12 @@ the old behavior, you can add it easily by subclassing Flask:: return self.response_class(*rv) return Flask.make_response(self, rv) +If you have an extension that was using :data:`~flask._request_ctx_stack` +before, please consider changing to :data:`~flask._app_ctx_stack` if it +makes sense for your extension. This will for example be the case for +extensions that connect to databases. This will allow your users to +easier use your extension with more complex use cases outside of requests. + Version 0.8 ----------- From d26af4fd6dd71793cf6373c1c18c82349494e0aa Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:38:08 +0100 Subject: [PATCH 1022/3747] Fixed some smaller things in the docs --- docs/appcontext.rst | 2 +- docs/reqcontext.rst | 2 +- flask/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index c331ffa5..e9e1ad8f 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -1,4 +1,4 @@ -.. _app_context: +.. _app-context: The Application Context ======================= diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 327afe6c..4da5acd8 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -6,7 +6,7 @@ The Request Context This document describes the behavior in Flask 0.7 which is mostly in line with the old behavior but has some small, subtle differences. -It is recommended that you read the :api:`app-context` chapter first. +It is recommended that you read the :ref:`app-context` chapter first. Diving into Context Locals -------------------------- diff --git a/flask/__init__.py b/flask/__init__.py index f35ef328..b91f9395 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -25,7 +25,7 @@ from .helpers import url_for, jsonify, json_available, flash, \ get_template_attribute, make_response, safe_join from .globals import current_app, g, request, session, _request_ctx_stack, \ _app_ctx_stack -from .ctx import has_request_context +from .ctx import has_request_context, has_app_context from .module import Module from .blueprints import Blueprint from .templating import render_template, render_template_string From bcd00e5070caf6f5ff7639d3758f5595bc5507a1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 9 Apr 2012 17:56:37 +0100 Subject: [PATCH 1023/3747] Fixed a typo --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 413ca884..16b03503 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -62,7 +62,7 @@ def has_request_context(): def has_app_context(): - """Worksl ike :func:`has_request_context` but for the application + """Works like :func:`has_request_context` but for the application context. You can also just do a boolean check on the :data:`current_app` object instead. From 9d09632dbfc0c08ab1a5290fbf282aad7d13e2f8 Mon Sep 17 00:00:00 2001 From: Sean Vieira Date: Mon, 9 Apr 2012 17:17:52 -0300 Subject: [PATCH 1024/3747] Fix spelling. --- flask/blueprints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index d81d3c73..9c557028 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -25,7 +25,7 @@ class BlueprintSetupState(object): #: a reference to the current application self.app = app - #: a reference to the blurprint that created this setup state. + #: a reference to the blueprint that created this setup state. self.blueprint = blueprint #: a dictionary with all options that were passed to the From acb61ae57b6fe9a82aaa9804c0e4bba8c2441746 Mon Sep 17 00:00:00 2001 From: Paul McMillan Date: Tue, 10 Apr 2012 13:14:38 -0700 Subject: [PATCH 1025/3747] Minor docs fix. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f7b6ee02..daaecb23 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -744,7 +744,7 @@ sessions work:: @app.route('/logout') def logout(): - # remove the username from the session if its there + # remove the username from the session if it's there session.pop('username', None) return redirect(url_for('index')) From a2eb5efcd8be834ca7e30e6392fa3a2067ad3a55 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Thu, 12 Apr 2012 19:14:52 +0300 Subject: [PATCH 1026/3747] few typos --- scripts/flaskext_compat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index 40c8c6b5..2f58ccc4 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -58,7 +58,7 @@ class ExtensionImporter(object): except ImportError: exc_type, exc_value, tb = sys.exc_info() # since we only establish the entry in sys.modules at the - # very this seems to be redundant, but if recursive imports + # end this seems to be redundant, but if recursive imports # happen we will call into the move import a second time. # On the second invocation we still don't have an entry for # fullname in sys.modules, but we will end up with the same @@ -71,7 +71,7 @@ class ExtensionImporter(object): # If it's an important traceback we reraise it, otherwise # we swallow it and try the next choice. The skipped frame - # is the one from __import__ above which we don't care about + # is the one from __import__ above which we don't care about. if self.is_important_traceback(realname, tb): raise exc_type, exc_value, tb.tb_next continue @@ -106,7 +106,7 @@ class ExtensionImporter(object): if module_name == important_module: return True - # Some python verisons will will clean up modules so early that the + # Some python versions will clean up modules so early that the # module name at that point is no longer set. Try guessing from # the filename then. filename = os.path.abspath(tb.tb_frame.f_code.co_filename) From ffbab00cd1c9ef89ab795b14b187334766556be7 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 17 Apr 2012 19:28:28 -0700 Subject: [PATCH 1027/3747] Rectified rampant 'roule'. --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index b09bcad5..c329852e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -586,7 +586,7 @@ with the route parameter the view function is defined with the decorator instead of the `view_func` parameter. =============== ========================================================== -`rule` the URL roule as string +`rule` the URL rule as string `endpoint` the endpoint for the registered URL rule. Flask itself assumes that the name of the view function is the name of the endpoint if not explicitly stated. From 0d2ffc094b45e717439c679255f4aecd018e24d0 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 18 Apr 2012 15:44:07 -0400 Subject: [PATCH 1028/3747] Use 'venv' consistently for virtualenv directory. Pointed out by tri on #pocoo. --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 791c99f1..8dcae5a7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -61,7 +61,7 @@ information about how to do that. Once you have it installed, run the same commands as above, but without the `sudo` prefix. Once you have virtualenv installed, just fire up a shell and create -your own environment. I usually create a project folder and an `env` +your own environment. I usually create a project folder and a `venv` folder within:: $ mkdir myproject From b885edf81013f05161525db1eaf4e64fc6f5c2c4 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 18 Apr 2012 15:54:04 -0400 Subject: [PATCH 1029/3747] Fix typo pointed out by tri on #pocoo. --- docs/patterns/appdispatch.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index a2d1176f..4f6dc18b 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -90,7 +90,7 @@ the dynamic application creation. The perfect level for abstraction in that regard is the WSGI layer. You write your own WSGI application that looks at the request that comes and -and delegates it to your Flask application. If that application does not +delegates it to your Flask application. If that application does not exist yet, it is dynamically created and remembered:: from threading import Lock From 10c34e6652fac704c9c77a47d4853896f3030e34 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 19 Apr 2012 11:50:08 -0400 Subject: [PATCH 1030/3747] Reword 0.9 upgrade doc, thanks to plaes on #pocoo. --- docs/upgrading.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 5955e552..7226d60e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -34,11 +34,12 @@ the old behavior, you can add it easily by subclassing Flask:: return self.response_class(*rv) return Flask.make_response(self, rv) -If you have an extension that was using :data:`~flask._request_ctx_stack` -before, please consider changing to :data:`~flask._app_ctx_stack` if it -makes sense for your extension. This will for example be the case for -extensions that connect to databases. This will allow your users to -easier use your extension with more complex use cases outside of requests. +If you maintain an extension that was using :data:`~flask._request_ctx_stack` +before, please consider changing to :data:`~flask._app_ctx_stack` if it makes +sense for your extension. For instance, the app context stack makes sense for +extensions which connect to databases. Using the app context stack instead of +the request stack will make extensions more readily handle use cases outside of +requests. Version 0.8 ----------- From a3cb2a33829ee517530d30cd920e5d652b358086 Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Thu, 19 Apr 2012 11:51:38 -0400 Subject: [PATCH 1031/3747] Use American English for "behavior" in docs. Prompted by plaes on #pocoo, mitsuhiko confirmed to use American English. --- CHANGES | 8 ++++---- docs/design.rst | 2 +- docs/extensiondev.rst | 4 ++-- docs/htmlfaq.rst | 2 +- docs/patterns/jquery.rst | 2 +- docs/patterns/mongokit.rst | 2 +- docs/quickstart.rst | 2 +- docs/templating.rst | 2 +- flask/app.py | 2 +- flask/helpers.py | 10 +++++----- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGES b/CHANGES index cb7b751e..83c1f4fe 100644 --- a/CHANGES +++ b/CHANGES @@ -153,7 +153,7 @@ Released on June 28th 2011, codename Grappa - Added :meth:`~flask.Flask.make_default_options_response` which can be used by subclasses to alter the default - behaviour for `OPTIONS` responses. + behavior for `OPTIONS` responses. - Unbound locals now raise a proper :exc:`RuntimeError` instead of an :exc:`AttributeError`. - Mimetype guessing and etag support based on file objects is now @@ -163,7 +163,7 @@ Released on June 28th 2011, codename Grappa - Static file handling for modules now requires the name of the static folder to be supplied explicitly. The previous autodetection was not reliable and caused issues on Google's App Engine. Until - 1.0 the old behaviour will continue to work but issue dependency + 1.0 the old behavior will continue to work but issue dependency warnings. - fixed a problem for Flask to run on jython. - added a `PROPAGATE_EXCEPTIONS` configuration variable that can be @@ -281,14 +281,14 @@ Released on July 6th 2010, codename Calvados the session cookie cross-subdomain wide. - autoescaping is no longer active for all templates. Instead it is only active for ``.html``, ``.htm``, ``.xml`` and ``.xhtml``. - Inside templates this behaviour can be changed with the + Inside templates this behavior can be changed with the ``autoescape`` tag. - refactored Flask internally. It now consists of more than a single file. - :func:`flask.send_file` now emits etags and has the ability to do conditional responses builtin. - (temporarily) dropped support for zipped applications. This was a - rarely used feature and led to some confusing behaviour. + rarely used feature and led to some confusing behavior. - added support for per-package template and static-file directories. - removed support for `create_jinja_loader` which is no longer used in 0.5 due to the improved module support. diff --git a/docs/design.rst b/docs/design.rst index 6ca363a6..cc247f3b 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -48,7 +48,7 @@ allocated will be freed again. Another thing that becomes possible when you have an explicit object lying around in your code is that you can subclass the base class -(:class:`~flask.Flask`) to alter specific behaviour. This would not be +(:class:`~flask.Flask`) to alter specific behavior. This would not be possible without hacks if the object were created ahead of time for you based on a class that is not exposed to you. diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 86c7c721..59ca76c5 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -40,7 +40,7 @@ that it works with multiple Flask application instances at once. This is a requirement because many people will use patterns like the :ref:`app-factories` pattern to create their application as needed to aid unittests and to support multiple configurations. Because of that it is -crucial that your application supports that kind of behaviour. +crucial that your application supports that kind of behavior. Most importantly the extension must be shipped with a `setup.py` file and registered on PyPI. Also the development checkout link should work so @@ -145,7 +145,7 @@ initialization functions: classes: Classes work mostly like initialization functions but can later be - used to further change the behaviour. For an example look at how the + used to further change the behavior. For an example look at how the `OAuth extension`_ works: there is an `OAuth` object that provides some helper functions like `OAuth.remote_app` to create a reference to a remote application that uses OAuth. diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst index 1da25f3d..b16f4cd5 100644 --- a/docs/htmlfaq.rst +++ b/docs/htmlfaq.rst @@ -52,7 +52,7 @@ Development of the HTML5 specification was started in 2004 under the name "Web Applications 1.0" by the Web Hypertext Application Technology Working Group, or WHATWG (which was formed by the major browser vendors Apple, Mozilla, and Opera) with the goal of writing a new and improved HTML -specification, based on existing browser behaviour instead of unrealistic +specification, based on existing browser behavior instead of unrealistic and backwards-incompatible specifications. For example, in HTML4 `` Date: Fri, 20 Apr 2012 09:07:58 -0400 Subject: [PATCH 1032/3747] Add detailed Apache httpd fastcgi configuration. --- docs/deploying/fastcgi.rst | 60 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index ebd68560..91824af0 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -51,6 +51,61 @@ can execute it: # chmod +x /var/www/yourapplication/yourapplication.fcgi +Configuring Apache +------------------ + +The example above is good enough for a basic Apache deployment but your `.fcgi` file will appear in your application URL e.g. www.example.com/yourapplication.fcgi/news/. There are few ways to resolve it. A preferable way is to use Apache ScriptAlias configuration directive:: + + <VirtualHost *> + ServerName example.com + ScriptAlias / /path/to/yourapplication.fcgi/ + </VirtualHost> + +Another way is to use a custom WSGI middleware. For example on a shared web hosting:: + + .htaccess + + <IfModule mod_fcgid.c> + AddHandler fcgid-script .fcgi + <Files ~ (\.fcgi)> + SetHandler fcgid-script + Options +FollowSymLinks +ExecCGI + </Files> + </IfModule> + + <IfModule mod_rewrite.c> + Options +FollowSymlinks + RewriteEngine On + RewriteBase / + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ yourapplication.fcgi/$1 [QSA,L] + </IfModule> + + yourapplication.fcgi + + #!/usr/bin/python + #: optional path to your local python site-packages folder + import sys + sys.path.insert(0, '<your_local_path>/lib/python2.6/site-packages') + + from flup.server.fcgi import WSGIServer + from yourapplication import app + + class ScriptNameStripper(object): + to_strip = '/yourapplication.fcgi' + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + environ['SCRIPT_NAME'] = '' + return self.app(environ, start_response) + + app = ScriptNameStripper(app) + + if __name__ == '__main__': + WSGIServer(app).run() + Configuring lighttpd -------------------- @@ -84,7 +139,6 @@ root. Also, see the Lighty docs for more information on `FastCGI and Python <http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ (note that explicitly passing a socket to run() is no longer necessary). - Configuring nginx ----------------- @@ -97,7 +151,7 @@ A basic flask FastCGI configuration for nginx looks like this:: location /yourapplication { try_files $uri @yourapplication; } location @yourapplication { include fastcgi_params; - fastcgi_split_path_info ^(/yourapplication)(.*)$; + fastcgi_split_path_info ^(/yourapplication)(.*)$; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; @@ -160,4 +214,4 @@ python path. Common problems are: .. _nginx: http://nginx.org/ .. _lighttpd: http://www.lighttpd.net/ .. _cherokee: http://www.cherokee-project.com/ -.. _flup: http://trac.saddi.com/flup +.. _flup: http://trac.saddi.com/flup \ No newline at end of file From fb011878857693659c6ec6f095fe3849803915e1 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Fri, 20 Apr 2012 09:20:20 -0400 Subject: [PATCH 1033/3747] Touch up fastcgi doc. --- docs/deploying/fastcgi.rst | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 91824af0..b2801560 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -54,16 +54,19 @@ can execute it: Configuring Apache ------------------ -The example above is good enough for a basic Apache deployment but your `.fcgi` file will appear in your application URL e.g. www.example.com/yourapplication.fcgi/news/. There are few ways to resolve it. A preferable way is to use Apache ScriptAlias configuration directive:: +The example above is good enough for a basic Apache deployment but your `.fcgi` +file will appear in your application URL +e.g. example.com/yourapplication.fcgi/news/. There are few ways to configure +your application so that yourapplication.fcgi does not appear in the URL. A +preferable way is to use the ScriptAlias configuration directive:: <VirtualHost *> ServerName example.com ScriptAlias / /path/to/yourapplication.fcgi/ </VirtualHost> -Another way is to use a custom WSGI middleware. For example on a shared web hosting:: - - .htaccess +If you cannot set ScriptAlias, for example on an shared web host, you can use +WSGI middleware to remove yourapplication.fcgi from the URLs. Set .htaccess:: <IfModule mod_fcgid.c> AddHandler fcgid-script .fcgi @@ -81,7 +84,7 @@ Another way is to use a custom WSGI middleware. For example on a shared web host RewriteRule ^(.*)$ yourapplication.fcgi/$1 [QSA,L] </IfModule> - yourapplication.fcgi +Set yourapplication.fcgi:: #!/usr/bin/python #: optional path to your local python site-packages folder @@ -128,16 +131,15 @@ A basic FastCGI configuration for lighttpd looks like that:: "^(/static.*)$" => "$1", "^(/.*)$" => "/yourapplication.fcgi$1" -Remember to enable the FastCGI, alias and rewrite modules. This -configuration binds the application to `/yourapplication`. If you want -the application to work in the URL root you have to work around a -lighttpd bug with the +Remember to enable the FastCGI, alias and rewrite modules. This configuration +binds the application to `/yourapplication`. If you want the application to +work in the URL root you have to work around a lighttpd bug with the :class:`~werkzeug.contrib.fixers.LighttpdCGIRootFix` middleware. Make sure to apply it only if you are mounting the application the URL -root. Also, see the Lighty docs for more information on `FastCGI and -Python <http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ -(note that explicitly passing a socket to run() is no longer necessary). +root. Also, see the Lighty docs for more information on `FastCGI and Python +<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ (note that +explicitly passing a socket to run() is no longer necessary). Configuring nginx ----------------- @@ -151,7 +153,7 @@ A basic flask FastCGI configuration for nginx looks like this:: location /yourapplication { try_files $uri @yourapplication; } location @yourapplication { include fastcgi_params; - fastcgi_split_path_info ^(/yourapplication)(.*)$; + fastcgi_split_path_info ^(/yourapplication)(.*)$; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param SCRIPT_NAME $fastcgi_script_name; fastcgi_pass unix:/tmp/yourapplication-fcgi.sock; @@ -214,4 +216,4 @@ python path. Common problems are: .. _nginx: http://nginx.org/ .. _lighttpd: http://www.lighttpd.net/ .. _cherokee: http://www.cherokee-project.com/ -.. _flup: http://trac.saddi.com/flup \ No newline at end of file +.. _flup: http://trac.saddi.com/flup From 0333c824bfa93fafabfb10b80f84d85abecda6bc Mon Sep 17 00:00:00 2001 From: Alex Vykalyuk <alekzvik@gmail.com> Date: Sat, 21 Apr 2012 22:27:24 +0300 Subject: [PATCH 1034/3747] Removed link to ep.io from quickstart --- docs/quickstart.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 46290d0b..b9cbdf32 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -839,7 +839,6 @@ immediately deploy to a hosted platform, all of which offer a free plan for small projects: - `Deploying Flask on Heroku <http://devcenter.heroku.com/articles/python>`_ -- `Deploying Flask on ep.io <https://www.ep.io/docs/quickstart/flask/>`_ - `Deploying WSGI on dotCloud <http://docs.dotcloud.com/services/python/>`_ with `Flask-specific notes <http://flask.pocoo.org/snippets/48/>`_ From d90f0afe39724040d0be92df054e2b1438886134 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Sat, 21 Apr 2012 18:40:02 -0400 Subject: [PATCH 1035/3747] Add test for jsonify padded=False, #495. --- flask/testsuite/helpers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index be268645..4781d2d9 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -73,14 +73,17 @@ class JSONTestCase(FlaskTestCase): @app.route('/dict') def return_dict(): return flask.jsonify(d) + @app.route("/unpadded") + def return_padded_false(): + return flask.jsonify(d, padded=False) @app.route("/padded") - def return_padded_json(): + def return_padded_true(): return flask.jsonify(d, padded=True) @app.route("/padded_custom") def return_padded_json_custom_callback(): return flask.jsonify(d, padded='my_func_name') c = app.test_client() - for url in '/kw', '/dict': + for url in '/kw', '/dict', '/unpadded': rv = c.get(url) self.assert_equal(rv.mimetype, 'application/json') self.assert_equal(flask.json.loads(rv.data), d) From 36194697ae8371f42d3041c1f89b6f2e77c34826 Mon Sep 17 00:00:00 2001 From: ekoka <verysimple@gmail.com> Date: Sat, 21 Apr 2012 23:36:08 -0300 Subject: [PATCH 1036/3747] Update flask/app.py --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 4c00c36b..4328c35b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1468,7 +1468,7 @@ class Flask(_PackageBoundObject): """ funcs = self.url_default_functions.get(None, ()) if '.' in endpoint: - bp = endpoint.split('.', 1)[0] + bp = endpoint.rsplit('.', 1)[0] funcs = chain(funcs, self.url_default_functions.get(bp, ())) for func in funcs: func(endpoint, values) From bb31188ec3882e9a6c6035b7a65ca257d424e31a Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Sun, 22 Apr 2012 12:30:15 -0400 Subject: [PATCH 1037/3747] Add a BuildError hook to url_for, #456. --- flask/app.py | 20 ++++++++++++++++++++ flask/helpers.py | 14 ++++++++++++++ flask/testsuite/basic.py | 12 ++++++++++++ 3 files changed, 46 insertions(+) diff --git a/flask/app.py b/flask/app.py index 4c00c36b..c0a9dac3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -329,6 +329,17 @@ class Flask(_PackageBoundObject): #: decorator. self.error_handler_spec = {None: self._error_handlers} + #: If not `None`, this function is called when :meth:`url_for` raises + #: :exc:`~werkzeug.routing.BuildError`, with the call signature:: + #: + #: self.build_error_handler(error, endpoint, **values) + #: + #: Here, `error` is the instance of `BuildError`, and `endpoint` and + #: `**values` are the arguments passed into :meth:`url_for`. + #: + #: .. versionadded:: 0.9 + self.build_error_handler = None + #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of #: the blueprint this function is active for, `None` for all requests. @@ -1473,6 +1484,15 @@ class Flask(_PackageBoundObject): for func in funcs: func(endpoint, values) + def handle_build_error(self, error, endpoint, **values): + """Handle :class:`~werkzeug.routing.BuildError` on :meth:`url_for`. + + Calls :attr:`build_error_handler` if it is not `None`. + """ + if self.build_error_handler is None: + raise error + return self.build_error_handler(error, endpoint, **values) + def preprocess_request(self): """Called before the actual request dispatching and will call every as :meth:`before_request` decorated function. diff --git a/flask/helpers.py b/flask/helpers.py index 238d7df7..21b010b4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -20,6 +20,7 @@ import mimetypes from time import time from zlib import adler32 from threading import RLock +from werkzeug.routing import BuildError from werkzeug.urls import url_quote # try to load the best simplejson implementation available. If JSON @@ -214,6 +215,10 @@ def url_for(endpoint, **values): .. versionadded:: 0.9 The `_anchor` and `_method` parameters were added. + .. versionadded:: 0.9 + Calls :meth:`Flask.handle_build_error` on + :exc:`~werkzeug.routing.BuildError`. + :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. @@ -260,6 +265,15 @@ def url_for(endpoint, **values): anchor = values.pop('_anchor', None) method = values.pop('_method', None) appctx.app.inject_url_defaults(endpoint, values) + try: + rv = url_adapter.build(endpoint, values, method=method, + force_external=external) + except BuildError, error: + values['_external'] = external + values['_anchor'] = anchor + values['_method'] = method + return appctx.app.handle_build_error(error, endpoint, **values) + rv = url_adapter.build(endpoint, values, method=method, force_external=external) if anchor is not None: diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 0a4b1d9c..d138c45e 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -19,6 +19,7 @@ from threading import Thread from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import BadRequest, NotFound from werkzeug.http import parse_date +from werkzeug.routing import BuildError class BasicFunctionalityTestCase(FlaskTestCase): @@ -695,6 +696,17 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(flask.url_for('hello', name='test x', _external=True), 'http://localhost/hello/test%20x') + def test_build_error_handler(self): + app = flask.Flask(__name__) + with app.test_request_context(): + self.assertRaises(BuildError, flask.url_for, 'spam') + def handler(error, endpoint, **values): + # Just a test. + return '/test_handler/' + app.build_error_handler = handler + with app.test_request_context(): + self.assert_equal(flask.url_for('spam'), '/test_handler/') + def test_custom_converters(self): from werkzeug.routing import BaseConverter class ListConverter(BaseConverter): From 8c8c524ddb791c2f7eed47b6d2e4317faf06a659 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Sun, 22 Apr 2012 12:51:31 -0400 Subject: [PATCH 1038/3747] Re-raise BuildError with traceback. --- flask/app.py | 6 +++++- flask/testsuite/basic.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index c0a9dac3..97dc5bde 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1490,7 +1490,11 @@ class Flask(_PackageBoundObject): Calls :attr:`build_error_handler` if it is not `None`. """ if self.build_error_handler is None: - raise error + exc_type, exc_value, tb = sys.exc_info() + if exc_value is error: + raise exc_type, exc_value, tb + else: + raise error return self.build_error_handler(error, endpoint, **values) def preprocess_request(self): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index d138c45e..cf7590cb 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -698,8 +698,23 @@ class BasicFunctionalityTestCase(FlaskTestCase): def test_build_error_handler(self): app = flask.Flask(__name__) + + # Test base case, a URL which results in a BuildError. with app.test_request_context(): self.assertRaises(BuildError, flask.url_for, 'spam') + + # Verify the error is re-raised if not the current exception. + try: + with app.test_request_context(): + flask.url_for('spam') + except BuildError, error: + pass + try: + raise RuntimeError('Test case where BuildError is not current.') + except RuntimeError: + self.assertRaises(BuildError, app.handle_build_error, error, 'spam') + + # Test a custom handler. def handler(error, endpoint, **values): # Just a test. return '/test_handler/' From 028229d0ff531db172f8cda52210de8903dbc14b Mon Sep 17 00:00:00 2001 From: Alex Vykalyuk <alekzvik@gmail.com> Date: Mon, 23 Apr 2012 00:32:48 +0300 Subject: [PATCH 1039/3747] Fixed typo in docs/quickstart --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b9cbdf32..e8b71ca9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -526,7 +526,7 @@ deal with that problem. To access parameters submitted in the URL (``?key=value``) you can use the :attr:`~flask.request.args` attribute:: - searchword = request.args.get('q', '') + searchword = request.args.get('key', '') We recommend accessing URL parameters with `get` or by catching the `KeyError` because users might change the URL and presenting them a 400 From 148c50abf983eb8bbd8ecb565c9f98912048af46 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Mon, 23 Apr 2012 21:20:47 -0400 Subject: [PATCH 1040/3747] Document url_for BuildError hook. --- flask/app.py | 1 + flask/helpers.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/flask/app.py b/flask/app.py index 97dc5bde..67383bbe 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1492,6 +1492,7 @@ class Flask(_PackageBoundObject): if self.build_error_handler is None: exc_type, exc_value, tb = sys.exc_info() if exc_value is error: + # exception is current, raise in context of original traceback. raise exc_type, exc_value, tb else: raise error diff --git a/flask/helpers.py b/flask/helpers.py index 21b010b4..5560d38c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -212,6 +212,40 @@ def url_for(endpoint, **values): For more information, head over to the :ref:`Quickstart <url-building>`. + To integrate applications, :class:`Flask` has a hook to intercept URL build + errors through :attr:`Flask.build_error_handler`. The `url_for` function + results in a :exc:`~werkzeug.routing.BuildError` when the current app does + not have a URL for the given endpoint and values. When it does, the + :data:`~flask.current_app` calls its :attr:`~Flask.build_error_handler` if + it is not `None`, which can return a string to use as the result of + `url_for` (instead of `url_for`'s default to raise the + :exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception. + An example:: + + def external_url_handler(error, endpoint, **values): + "Looks up an external URL when `url_for` cannot build a URL." + # This is an example of hooking the build_error_handler. + # Here, lookup_url is some utility function you've built + # which looks up the endpoint in some external URL registry. + url = lookup_url(endpoint, **values) + if url is None: + # External lookup did not have a URL. + # Re-raise the BuildError, in context of original traceback. + exc_type, exc_value, tb = sys.exc_info() + if exc_value is error: + raise exc_type, exc_value, tb + else: + raise error + # url_for will use this result, instead of raising BuildError. + return url + + app.build_error_handler = external_url_handler + + Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and + `endpoint` and `**values` are the arguments passed into `url_for`. Note + that this is for building URLs outside the current application, and not for + handling 404 NotFound errors. + .. versionadded:: 0.9 The `_anchor` and `_method` parameters were added. From 2262ce4915aec0dfffa8e71244ebe10f72d11111 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Mon, 23 Apr 2012 21:36:28 -0400 Subject: [PATCH 1041/3747] Skip template leak test when not CPython2.7, #452. --- flask/testsuite/regression.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index 51a866a4..bc37afc4 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -72,9 +72,12 @@ class MemoryTestCase(FlaskTestCase): # Trigger caches fire() - with self.assert_no_leak(): - for x in xrange(10): - fire() + # This test only works on CPython 2.7. + if sys.version_info >= (2, 7) and \ + not hasattr(sys, 'pypy_translation_info'): + with self.assert_no_leak(): + for x in xrange(10): + fire() def suite(): From b31f2d9a640c154e41de6d9631e95cf105e96e1f Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Mon, 23 Apr 2012 21:46:53 -0400 Subject: [PATCH 1042/3747] Require Werkzeug>=0.7, #449. --- README | 9 +++++---- setup.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README b/README index 7d5ada23..317080a7 100644 --- a/README +++ b/README @@ -17,10 +17,11 @@ ~ What do I need? - Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will - install them for you if you do `easy_install Flask`. - I encourage you to use a virtualenv. Check the docs for - complete installation and usage instructions. + Jinja 2.4 and Werkzeug 0.7 or later. + `pip` or `easy_install` will install them for you if you do + `pip install Flask`. I encourage you to use a virtualenv. + Check the docs for complete installation and usage + instructions. ~ Where are the docs? diff --git a/setup.py b/setup.py index 8169a517..fdc4653e 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ setup( zip_safe=False, platforms='any', install_requires=[ - 'Werkzeug>=0.6.1', + 'Werkzeug>=0.7', 'Jinja2>=2.4' ], classifiers=[ From ff5ee034b8c71a79d3f29c7b7a1ad27f6a8893e3 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Mon, 23 Apr 2012 21:47:28 -0400 Subject: [PATCH 1043/3747] Touch up README. --- README | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README b/README index 317080a7..7297f5db 100644 --- a/README +++ b/README @@ -6,7 +6,7 @@ ~ What is Flask? Flask is a microframework for Python based on Werkzeug - and Jinja2. It's intended for small scale applications + and Jinja2. It's intended for getting started very quickly and was developed with best intentions in mind. ~ Is it ready? @@ -51,3 +51,5 @@ Either use the #pocoo IRC channel on irc.freenode.net or ask on the mailinglist: http://flask.pocoo.org/mailinglist/ + + See http://flask.pocoo.org/community/ for more resources. From 7c79ce6e418f07a49be8c25a4c5a40e5347be257 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Mon, 23 Apr 2012 23:42:58 -0400 Subject: [PATCH 1044/3747] Revise foreword and Becoming Big docs, #484. --- docs/advanced_foreword.rst | 65 ++++++++++++++-------------- docs/becomingbig.rst | 88 ++++++++++++++++++++++---------------- docs/contents.rst.inc | 1 + docs/foreword.rst | 66 ++++++++++++++-------------- 4 files changed, 118 insertions(+), 102 deletions(-) diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst index cc1a1843..1831dd59 100644 --- a/docs/advanced_foreword.rst +++ b/docs/advanced_foreword.rst @@ -1,27 +1,26 @@ +.. _advanced_foreword: + Foreword for Experienced Programmers ==================================== -This chapter is for programmers who have worked with other frameworks in the -past, and who may have more specific or esoteric concerns that the typical -user. +Thread-Locals in Flask +---------------------- -Threads in Flask ----------------- - -One of the design decisions with Flask was that simple tasks should be simple; +One of the design decisions in Flask was that simple tasks should be simple; they should not take a lot of code and yet they should not limit you. Because -of that we made a few design choices that some people might find surprising or -unorthodox. For example, Flask uses thread-local objects internally so that -you don’t have to pass objects around from function to function within a -request in order to stay threadsafe. While this is a really easy approach and -saves you a lot of time, it might also cause some troubles for very large -applications because changes on these thread-local objects can happen anywhere -in the same thread. In order to solve these problems we don’t hide the thread -locals for you but instead embrace them and provide you with a lot of tools to -make it as pleasant as possible to work with them. +of that, Flask has few design choices that some people might find surprising or +unorthodox. For example, Flask uses thread-local objects internally so that you +don’t have to pass objects around from function to function within a request in +order to stay threadsafe. This approach is convenient, but requires a valid +request context for dependency injection or when attempting to reuse code which +uses a value pegged to the request. The Flask project is honest about +thread-locals, does not hide them, and calls out in the code and documentation +where they are used. -Web Development is Dangerous ----------------------------- +Develop for the Web with Caution +-------------------------------- + +Always keep security in mind when building web applications. If you write a web application, you are probably allowing users to register and leave their data on your server. The users are entrusting you with data. @@ -30,22 +29,22 @@ you still want that data to be stored securely. Unfortunately, there are many ways the security of a web application can be compromised. Flask protects you against one of the most common security -problems of modern web applications: cross-site scripting (XSS). Unless -you deliberately mark insecure HTML as secure, Flask and the underlying -Jinja2 template engine have you covered. But there are many more ways to -cause security problems. +problems of modern web applications: cross-site scripting (XSS). Unless you +deliberately mark insecure HTML as secure, Flask and the underlying Jinja2 +template engine have you covered. But there are many more ways to cause +security problems. -The documentation will warn you about aspects of web development that -require attention to security. Some of these security concerns -are far more complex than one might think, and we all sometimes underestimate -the likelihood that a vulnerability will be exploited - until a clever -attacker figures out a way to exploit our applications. And don't think -that your application is not important enough to attract an attacker. -Depending on the kind of attack, chances are that automated bots are -probing for ways to fill your database with spam, links to malicious -software, and the like. +The documentation will warn you about aspects of web development that require +attention to security. Some of these security concerns are far more complex +than one might think, and we all sometimes underestimate the likelihood that a +vulnerability will be exploited - until a clever attacker figures out a way to +exploit our applications. And don't think that your application is not +important enough to attract an attacker. Depending on the kind of attack, +chances are that automated bots are probing for ways to fill your database with +spam, links to malicious software, and the like. -So always keep security in mind when doing web development. +Flask is no different from any other framework in that you the developer must +build with caution, watching for exploits when building to your requirements. The Status of Python 3 ---------------------- @@ -65,3 +64,5 @@ using Python 2.6 and 2.7 with activated Python 3 warnings during development. If you plan on upgrading to Python 3 in the near future we strongly recommend that you read `How to write forwards compatible Python code <http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/>`_. + +Continue to :ref:`installation` or the :ref:`quickstart`. diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 20a0186e..ca803060 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -3,45 +3,57 @@ Becoming Big ============ -Your application is becoming more and more complex? If you suddenly -realize that Flask does things in a way that does not work out for your -application there are ways to deal with that. +Here are your options when growing your codebase or scaling your application. -Flask is powered by Werkzeug and Jinja2, two libraries that are in use at -a number of large websites out there and all Flask does is bring those -two together. Being a microframework Flask does not do much more than -combining existing libraries - there is not a lot of code involved. -What that means for large applications is that it's very easy to take the -code from Flask and put it into a new module within the applications and -expand on that. +Read the Source. +---------------- -Flask is designed to be extended and modified in a couple of different -ways: +Flask started in part to demonstrate how to build your own framework on top of +existing well-used tools Werkzeug (WSGI) and Jinja (templating), and as it +developed, it became useful to a wide audience. As you grow your codebase, +don't just use Flask -- understand it. Read the source. Flask's code is +written to be read; it's documentation published so you can use its internal +APIs. Flask sticks to documented APIs in upstream libraries, and documents its +internal utilities so that you can find the hook points needed for your +project. -- Flask extensions. For a lot of reusable functionality you can create - extensions. For extensions a number of hooks exist throughout Flask - with signals and callback functions. +Hook. Extend. +------------- -- Subclassing. The majority of functionality can be changed by creating - a new subclass of the :class:`~flask.Flask` class and overriding - methods provided for this exact purpose. +The :ref:`api` docs are full of available overrides, hook points, and +:ref:`signals`. You can provide custom classes for things like the request and +response objects. Dig deeper on the APIs you use, and look for the +customizations which are available out of the box in a Flask release. Look for +ways in which your project can be refactored into a collection of utilities and +Flask extensions. Explore the many extensions in the community, and look for +patterns to build your own extensions if you do not find the tools you need. -- Forking. If nothing else works out you can just take the Flask - codebase at a given point and copy/paste it into your application - and change it. Flask is designed with that in mind and makes this - incredible easy. You just have to take the package and copy it - into your application's code and rename it (for example to - `framework`). Then you can start modifying the code in there. +Subclass. +--------- -Why consider Forking? +The :class:`~flask.Flask` class has many methods designed for subclassing. You +can quickly add or customize behavior by subclassing :class:`~flask.Flask` (see +the linked method docs) and using that subclass wherever you instantiate an +application class. This works well with :ref:`app-factories`. + +Wrap with middleware. --------------------- -The majority of code of Flask is within Werkzeug and Jinja2. These -libraries do the majority of the work. Flask is just the paste that glues -those together. For every project there is the point where the underlying -framework gets in the way (due to assumptions the original developers -had). This is natural because if this would not be the case, the -framework would be a very complex system to begin with which causes a +The :ref:`app-dispatch` chapter shows in detail how to apply middleware. You +can introduce WSGI middleware to wrap your Flask instances and introduce fixes +and changes at the layer between your Flask application and your HTTP +server. Werkzeug includes several `middlewares +<http://werkzeug.pocoo.org/docs/middlewares/>`_. + +Fork. +----- + +If none of the above options work, fork Flask. The majority of code of Flask +is within Werkzeug and Jinja2. These libraries do the majority of the work. +Flask is just the paste that glues those together. For every project there is +the point where the underlying framework gets in the way (due to assumptions +the original developers had). This is natural because if this would not be the +case, the framework would be a very complex system to begin with which causes a steep learning curve and a lot of user frustration. This is not unique to Flask. Many people use patched and modified @@ -55,8 +67,8 @@ Furthermore integrating upstream changes can be a complex process, depending on the number of changes. Because of that, forking should be the very last resort. -Scaling like a Pro ------------------- +Scale like a pro. +----------------- For many web applications the complexity of the code is less an issue than the scaling for the number of users or data entries expected. Flask by @@ -78,11 +90,11 @@ majority of servers are using either threads, greenlets or separate processes to achieve concurrency which are all methods well supported by the underlying Werkzeug library. -Dialogue with the Community +Discuss with the community. --------------------------- -The Flask developers are very interested to keep everybody happy, so as -soon as you find an obstacle in your way, caused by Flask, don't hesitate -to contact the developers on the mailinglist or IRC channel. The best way -for the Flask and Flask-extension developers to improve it for larger +The Flask developers keep the framework accessible to users with codebases big +and small. If you find an obstacle in your way, caused by Flask, don't hesitate +to contact the developers on the mailinglist or IRC channel. The best way for +the Flask and Flask extension developers to improve the tools for larger applications is getting feedback from users. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index a1893c48..b60c7a03 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -9,6 +9,7 @@ instructions for web development with Flask. :maxdepth: 2 foreword + advanced_foreword installation quickstart tutorial/index diff --git a/docs/foreword.rst b/docs/foreword.rst index b186aba6..167f2f41 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -8,48 +8,50 @@ should or should not be using it. What does "micro" mean? ----------------------- -“Micro” does not mean that your whole web application has to fit into -a single Python file (although it certainly can). Nor does it mean -that Flask is lacking in functionality. The "micro" in microframework -means Flask aims to keep the core simple but extensible. Flask won't make -many decisions for you, such as what database to use. Those decisions that -it does make, such as what templating engine to use, are easy to change. -Everything else is up to you, so that Flask can be everything you need -and nothing you don't. +“Micro” does not mean that your whole web application has to fit into a single +Python file, although it certainly can. Nor does it mean that Flask is lacking +in functionality. The "micro" in microframework means Flask aims to keep the +core simple but extensible. Flask won't make many decisions for you, such as +what database to use. Those decisions that it does make, such as what +templating engine to use, are easy to change. Everything else is up to you, so +that Flask can be everything you need and nothing you don't. By default, Flask does not include a database abstraction layer, form validation or anything else where different libraries already exist that can -handle that. Instead, FLask extensions add such functionality to your -application as if it was implemented in Flask itself. Numerous extensions +handle that. Instead, Flask supports extensions to add such functionality to +your application as if it was implemented in Flask itself. Numerous extensions provide database integration, form validation, upload handling, various open -authentication technologies, and more. Flask may be "micro", but the -possibilities are endless. +authentication technologies, and more. Flask may be "micro", but it's ready for +production use on a variety of needs. -Convention over Configuration +Configuration and Conventions ----------------------------- -Flask is based on convention over configuration, which means that many things -are preconfigured. For example, by convention templates and static files are -stored in subdirectories within the application's Python source tree. While -this can be changed you usually don't have to. We want to minimize the time -you need to spend in order to get up and running, without assuming things -about your needs. +Flask has many configuration values, with sensible defaults, and a few +conventions when getting started. By convention templates and static files are +stored in subdirectories within the application's Python source tree, with the +names `templates` and `static` respectively. While this can be changed you +usually don't have to, especially when getting started. -Growing Up ----------- +Growing with Flask +------------------ -Since Flask is based on a very solid foundation there is not a lot of code in -Flask itself. As such it's easy to adapt even for large applications and we -are making sure that you can either configure it as much as possible by -subclassing things or by forking the entire codebase. If you are interested -in that, check out the :ref:`becomingbig` chapter. +Once you have Flask up and running, you'll find a variety of extensions +available in the community to integrate your project for production. The Flask +core team reviews extensions and ensures approved extensions do not break with +future releases. -If you are curious about the Flask design principles, head over to the section -about :ref:`design`. +As your codebase grows, you are free to make the design decisions appropriate +for your project. Flask will continue to provide a very simple glue layer to +the best that Python has to offer. You can implement advanced patterns in +SQLAlchemy or another database tool, introduce non-relational data persistence +as appropriate, and take advantage of framework-agnostic tools built for WSGI, +the Python web interface. -For the Stalwart and Wizened... -------------------------------- +Flask includes many hooks to customize its behavior. Should you need more +customization, the Flask class is built for subclassing. If you are interested +in that, check out the :ref:`becomingbig` chapter. If you are curious about +the Flask design principles, head over to the section about :ref:`design`. -If you're more curious about the minutiae of Flask's implementation, and -whether its structure is right for your needs, read the +Continue to :ref:`installation`, the :ref:`quickstart`, or the :ref:`advanced_foreword`. From 26da6a5365e1fd229932f167a299128f30fae154 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Tue, 24 Apr 2012 01:48:05 -0400 Subject: [PATCH 1045/3747] Use default send_file max-age consistently. Prior to this commit, the send_file max-age hook and config were only used for the static file handler. Now they are used when calling helpers.send_file directly. --- CHANGES | 17 ++++++------ docs/config.rst | 11 +++++--- flask/app.py | 6 ----- flask/helpers.py | 51 ++++++++++++++++++++++------------- flask/testsuite/blueprints.py | 23 ++++++++++++++++ flask/testsuite/helpers.py | 25 +++++++++++------ 6 files changed, 88 insertions(+), 45 deletions(-) diff --git a/CHANGES b/CHANGES index 83c1f4fe..537f6544 100644 --- a/CHANGES +++ b/CHANGES @@ -53,14 +53,15 @@ Relase date to be decided, codename to be chosen. - View functions can now return a tuple with the first instance being an instance of :class:`flask.Response`. This allows for returning ``jsonify(error="error msg"), 400`` from a view function. -- :class:`flask.Flask` now provides a `get_send_file_options` hook for - subclasses to override behavior of serving static files from Flask when using - :meth:`flask.Flask.send_static_file` based on keywords in - :func:`flask.helpers.send_file`. This hook is provided a filename, which for - example allows changing cache controls by file extension. The default - max-age for `send_static_file` can be configured through a new - ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, regardless of whether - the `get_send_file_options` hook is used. +- :class:`~flask.Flask` and :class:`~flask.Blueprint` now provide a + :meth:`~flask.Flask.get_send_file_max_age` hook for subclasses to override + behavior of serving static files from Flask when using + :meth:`flask.Flask.send_static_file` (used for the default static file + handler) and :func:`~flask.helpers.send_file`. This hook is provided a + filename, which for example allows changing cache controls by file extension. + The default max-age for `send_file` and static files can be configured + through a new ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, which is + used in the default `get_send_file_max_age` implementation. - Fixed an assumption in sessions implementation which could break message flashing on sessions implementations which use external storage. - Changed the behavior of tuple return values from functions. They are no diff --git a/docs/config.rst b/docs/config.rst index 86bfb0d1..7a32fb84 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -111,12 +111,15 @@ The following configuration values are used internally by Flask: content length greater than this by returning a 413 status code. ``SEND_FILE_MAX_AGE_DEFAULT``: Default cache control max age to use with - :meth:`flask.Flask.send_static_file`, in + :meth:`~flask.Flask.send_static_file` (the + default static file handler) and + :func:`~flask.send_file`, in seconds. Override this value on a per-file basis using the - :meth:`flask.Flask.get_send_file_options` and - :meth:`flask.Blueprint.get_send_file_options` - hooks. Defaults to 43200 (12 hours). + :meth:`~flask.Flask.get_send_file_max_age` + hook on :class:`~flask.Flask` or + :class:`~flask.Blueprint`, + respectively. Defaults to 43200 (12 hours). ``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will not execute the error handlers of HTTP exceptions but instead treat the diff --git a/flask/app.py b/flask/app.py index 67383bbe..5f809abb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1041,12 +1041,6 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .append((code_or_exception, f)) - def get_send_file_options(self, filename): - # Override: Hooks in SEND_FILE_MAX_AGE_DEFAULT config. - options = super(Flask, self).get_send_file_options(filename) - options['cache_timeout'] = self.config['SEND_FILE_MAX_AGE_DEFAULT'] - return options - @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. diff --git a/flask/helpers.py b/flask/helpers.py index 5560d38c..05f84ef7 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -406,7 +406,7 @@ def get_flashed_messages(with_categories=False, category_filter=[]): def send_file(filename_or_fp, mimetype=None, as_attachment=False, attachment_filename=None, add_etags=True, - cache_timeout=60 * 60 * 12, conditional=False): + cache_timeout=None, conditional=False): """Sends the contents of a file to the client. This will use the most efficient method available and configured. By default it will try to use the WSGI server's file_wrapper support. Alternatively @@ -420,10 +420,6 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, guessing requires a `filename` or an `attachment_filename` to be provided. - Note `get_send_file_options` in :class:`flask.Flask` hooks the - ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable to set the default - cache_timeout. - Please never pass filenames to this function from user sources without checking them first. Something like this is usually sufficient to avoid security problems:: @@ -443,6 +439,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, able to, otherwise attach an etag yourself. This functionality will be removed in Flask 1.0 + .. versionchanged:: 0.9 + cache_timeout pulls its default from application config, when None. + :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. @@ -459,7 +458,11 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, differs from the file's filename. :param add_etags: set to `False` to disable attaching of etags. :param conditional: set to `True` to enable conditional responses. - :param cache_timeout: the timeout in seconds for the headers. + + :param cache_timeout: the timeout in seconds for the headers. When `None` + (default), this value is set by + :meth:`~Flask.get_send_file_max_age` of + :data:`~flask.current_app`. """ mtime = None if isinstance(filename_or_fp, basestring): @@ -523,6 +526,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, rv.last_modified = int(mtime) rv.cache_control.public = True + if cache_timeout is None: + cache_timeout = current_app.get_send_file_max_age(filename) if cache_timeout: rv.cache_control.max_age = cache_timeout rv.expires = int(time() + cache_timeout) @@ -757,26 +762,31 @@ class _PackageBoundObject(object): return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) - def get_send_file_options(self, filename): - """Provides keyword arguments to send to :func:`send_from_directory`. + def get_send_file_max_age(self, filename): + """Provides default cache_timeout for the :func:`send_file` functions. + + By default, this function returns ``SEND_FILE_MAX_AGE_DEFAULT`` from + the configuration of :data:`~flask.current_app`. + + Static file functions such as :func:`send_from_directory` use this + function, and :func:`send_file` calls this function on + :data:`~flask.current_app` when the given cache_timeout is `None`. If a + cache_timeout is given in :func:`send_file`, that timeout is used; + otherwise, this method is called. This allows subclasses to change the behavior when sending files based on the filename. For example, to set the cache timeout for .js files - to 60 seconds (note the options are keywords for :func:`send_file`):: + to 60 seconds:: class MyFlask(flask.Flask): - def get_send_file_options(self, filename): - options = super(MyFlask, self).get_send_file_options(filename) - if filename.lower().endswith('.js'): - options['cache_timeout'] = 60 - options['conditional'] = True - return options + def get_send_file_max_age(self, name): + if name.lower().endswith('.js'): + return 60 + return flask.Flask.get_send_file_max_age(self, name) .. versionadded:: 0.9 """ - options = {} - options['cache_timeout'] = current_app.config['SEND_FILE_MAX_AGE_DEFAULT'] - return options + return current_app.config['SEND_FILE_MAX_AGE_DEFAULT'] def send_static_file(self, filename): """Function used internally to send static files from the static @@ -786,8 +796,11 @@ class _PackageBoundObject(object): """ if not self.has_static_folder: raise RuntimeError('No static folder for this object') + # Ensure get_send_file_max_age is called in all cases. + # Here, we ensure get_send_file_max_age is called for Blueprints. + cache_timeout = self.get_send_file_max_age(filename) return send_from_directory(self.static_folder, filename, - **self.get_send_file_options(filename)) + cache_timeout=cache_timeout) def open_resource(self, resource, mode='rb'): """Opens a resource from the application's resource folder. To see diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 5f3d3ab3..c9622121 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -386,6 +386,29 @@ class BlueprintTestCase(FlaskTestCase): with flask.Flask(__name__).test_request_context(): self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') + def test_default_static_cache_timeout(self): + app = flask.Flask(__name__) + class MyBlueprint(flask.Blueprint): + def get_send_file_max_age(self, filename): + return 100 + + blueprint = MyBlueprint('blueprint', __name__, static_folder='static') + app.register_blueprint(blueprint) + + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT'] + try: + with app.test_request_context(): + unexpected_max_age = 3600 + if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == unexpected_max_age: + unexpected_max_age = 7200 + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = unexpected_max_age + rv = blueprint.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 100) + finally: + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default + def test_templates_list(self): from blueprintapp import app templates = sorted(app.jinja_env.list_templates()) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 4781d2d9..a0e60aac 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -237,28 +237,37 @@ class SendfileTestCase(FlaskTestCase): app = flask.Flask(__name__) # default cache timeout is 12 hours with app.test_request_context(): + # Test with static file handler. rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 12 * 60 * 60) + # Test again with direct use of send_file utility. + rv = flask.send_file('static/index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 12 * 60 * 60) app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600 with app.test_request_context(): + # Test with static file handler. rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 3600) - # override get_send_file_options with some new values and check them + # Test again with direct use of send_file utility. + rv = flask.send_file('static/index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 3600) class StaticFileApp(flask.Flask): - def get_send_file_options(self, filename): - opts = super(StaticFileApp, self).get_send_file_options(filename) - opts['cache_timeout'] = 10 - # this test catches explicit inclusion of the conditional - # keyword arg in the guts - opts['conditional'] = True - return opts + def get_send_file_max_age(self, filename): + return 10 app = StaticFileApp(__name__) with app.test_request_context(): + # Test with static file handler. rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 10) + # Test again with direct use of send_file utility. + rv = flask.send_file('static/index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 10) class LoggingTestCase(FlaskTestCase): From 33bae1a8dcadbf56fb19ad814fa516b6468bb2cb Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Wed, 18 Apr 2012 20:46:07 -0400 Subject: [PATCH 1046/3747] Add Flask.request_globals_class to customize g. Requested by toothr on #pocoo. --- CHANGES | 2 ++ flask/app.py | 7 ++++++- flask/ctx.py | 3 ++- flask/testsuite/appctx.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 537f6544..10cd1c08 100644 --- a/CHANGES +++ b/CHANGES @@ -66,6 +66,8 @@ Relase date to be decided, codename to be chosen. flashing on sessions implementations which use external storage. - Changed the behavior of tuple return values from functions. They are no longer arguments to the response object, they now have a defined meaning. +- Added :attr:`flask.Flask.request_globals_class` to allow a specific class to + be used on creation of the :data:`~flask.g` instance of each request. Version 0.8.1 ------------- diff --git a/flask/app.py b/flask/app.py index 5f809abb..1a2961e1 100644 --- a/flask/app.py +++ b/flask/app.py @@ -28,7 +28,7 @@ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ find_package from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import RequestContext, AppContext +from .ctx import RequestContext, AppContext, _RequestGlobals from .globals import _request_ctx_stack, request from .sessions import SecureCookieSessionInterface from .module import blueprint_is_module @@ -148,6 +148,11 @@ class Flask(_PackageBoundObject): #: :class:`~flask.Response` for more information. response_class = Response + #: The class that is used for the :data:`~flask.g` instance. + #: + #: .. versionadded:: 0.9 + request_globals_class = _RequestGlobals + #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload diff --git a/flask/ctx.py b/flask/ctx.py index 16b03503..cf197d05 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -18,6 +18,7 @@ from .module import blueprint_is_module class _RequestGlobals(object): + """A plain object.""" pass @@ -139,7 +140,7 @@ class RequestContext(object): self.app = app self.request = app.request_class(environ) self.url_adapter = app.create_url_adapter(self.request) - self.g = _RequestGlobals() + self.g = app.request_globals_class() self.flashes = None self.session = None diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index a4ad479b..1dcdb406 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -65,6 +65,16 @@ class AppContextTestCase(FlaskTestCase): self.assert_equal(cleanup_stuff, [None]) + def test_custom_request_globals_class(self): + class CustomRequestGlobals(object): + def __init__(self): + self.spam = 'eggs' + app = flask.Flask(__name__) + app.request_globals_class = CustomRequestGlobals + with app.test_request_context(): + self.assert_equal( + flask.render_template_string('{{ g.spam }}'), 'eggs') + def suite(): suite = unittest.TestSuite() From e78e2a1641e5b7ad538d93154ee59445f4d4eaf7 Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Tue, 24 Apr 2012 02:10:16 -0400 Subject: [PATCH 1047/3747] Document example request_globals_class use cases. --- flask/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask/app.py b/flask/app.py index 1a2961e1..aa64f4c6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -150,6 +150,13 @@ class Flask(_PackageBoundObject): #: The class that is used for the :data:`~flask.g` instance. #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on expected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: #: .. versionadded:: 0.9 request_globals_class = _RequestGlobals From 12dcba8849d153c7e13e99b6bcf57922e1a97240 Mon Sep 17 00:00:00 2001 From: ekoka <verysimple@gmail.com> Date: Tue, 24 Apr 2012 05:32:52 -0300 Subject: [PATCH 1048/3747] Update flask/testsuite/basic.py --- flask/testsuite/basic.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 0a4b1d9c..c2acca57 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -911,6 +911,29 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(c.get('/de/').data, '/de/about') self.assert_equal(c.get('/de/about').data, '/foo') self.assert_equal(c.get('/foo').data, '/en/about') + + def test_inject_blueprint_url_defaults(self): + app = flask.Flask(__name__) + bp = flask.Blueprint('foo.bar.baz', __name__, + template_folder='template') + + @bp.url_defaults + def bp_defaults(endpoint, values): + values['page'] = 'login' + @bp.route('/<page>') + def view(page): pass + + app.register_blueprint(bp) + + values = dict() + app.inject_url_defaults('foo.bar.baz.view', values) + expected = dict(page='login') + self.assert_equal(values, expected) + + with app.test_request_context('/somepage'): + url = flask.url_for('foo.bar.baz.view') + expected = '/login' + self.assert_equal(url, expected) def test_debug_mode_complains_after_first_request(self): app = flask.Flask(__name__) From 2053d04db0f303430f5f6c5bc6e97b8bec46c399 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 8 May 2012 11:56:11 +0100 Subject: [PATCH 1049/3747] Improved interface for the URL build error handler --- flask/app.py | 43 +++++++++++++++++++++------------------- flask/helpers.py | 5 +++-- flask/testsuite/basic.py | 6 +++--- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/flask/app.py b/flask/app.py index aa64f4c6..40090ab0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -19,7 +19,7 @@ from itertools import chain from functools import update_wrapper from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule, RequestRedirect +from werkzeug.routing import Map, Rule, RequestRedirect, BuildError from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed, BadRequest @@ -341,16 +341,14 @@ class Flask(_PackageBoundObject): #: decorator. self.error_handler_spec = {None: self._error_handlers} - #: If not `None`, this function is called when :meth:`url_for` raises - #: :exc:`~werkzeug.routing.BuildError`, with the call signature:: - #: - #: self.build_error_handler(error, endpoint, **values) - #: - #: Here, `error` is the instance of `BuildError`, and `endpoint` and - #: `**values` are the arguments passed into :meth:`url_for`. + #: A list of functions that are called when :meth:`url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function registered here + #: is called with `error`, `endpoint` and `values`. If a function + #: returns `None` or raises a `BuildError` the next function is + #: tried. #: #: .. versionadded:: 0.9 - self.build_error_handler = None + self.url_build_error_handlers = [] #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of @@ -1490,19 +1488,24 @@ class Flask(_PackageBoundObject): for func in funcs: func(endpoint, values) - def handle_build_error(self, error, endpoint, **values): + def handle_url_build_error(self, error, endpoint, values): """Handle :class:`~werkzeug.routing.BuildError` on :meth:`url_for`. - - Calls :attr:`build_error_handler` if it is not `None`. """ - if self.build_error_handler is None: - exc_type, exc_value, tb = sys.exc_info() - if exc_value is error: - # exception is current, raise in context of original traceback. - raise exc_type, exc_value, tb - else: - raise error - return self.build_error_handler(error, endpoint, **values) + exc_type, exc_value, tb = sys.exc_info() + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + if rv is not None: + return rv + except BuildError, error: + pass + + # At this point we want to reraise the exception. If the error is + # still the same one we can reraise it with the original traceback, + # otherwise we raise it from here. + if error is exc_value: + raise exc_type, exc_value, tb + raise error def preprocess_request(self): """Called before the actual request dispatching and will diff --git a/flask/helpers.py b/flask/helpers.py index 05f84ef7..e633e1b9 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -11,7 +11,6 @@ from __future__ import with_statement -import imp import os import sys import pkgutil @@ -303,10 +302,12 @@ def url_for(endpoint, **values): rv = url_adapter.build(endpoint, values, method=method, force_external=external) except BuildError, error: + # We need to inject the values again so that the app callback can + # deal with that sort of stuff. values['_external'] = external values['_anchor'] = anchor values['_method'] = method - return appctx.app.handle_build_error(error, endpoint, **values) + return appctx.app.handle_url_build_error(error, endpoint, values) rv = url_adapter.build(endpoint, values, method=method, force_external=external) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index cf7590cb..55b66f78 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -712,13 +712,13 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: raise RuntimeError('Test case where BuildError is not current.') except RuntimeError: - self.assertRaises(BuildError, app.handle_build_error, error, 'spam') + self.assertRaises(BuildError, app.handle_url_build_error, error, 'spam', {}) # Test a custom handler. - def handler(error, endpoint, **values): + def handler(error, endpoint, values): # Just a test. return '/test_handler/' - app.build_error_handler = handler + app.url_build_error_handlers.append(handler) with app.test_request_context(): self.assert_equal(flask.url_for('spam'), '/test_handler/') From dbfd406a21191c26c2987ffd11f7c49b8733cd82 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 8 May 2012 12:51:26 +0100 Subject: [PATCH 1050/3747] Added required_methods --- CHANGES | 2 ++ docs/api.rst | 4 ++++ flask/app.py | 9 ++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 10cd1c08..2c3f904a 100644 --- a/CHANGES +++ b/CHANGES @@ -68,6 +68,8 @@ Relase date to be decided, codename to be chosen. longer arguments to the response object, they now have a defined meaning. - Added :attr:`flask.Flask.request_globals_class` to allow a specific class to be used on creation of the :data:`~flask.g` instance of each request. +- Added `required_methods` attribute to view functions to force-add methods + on registration. Version 0.8.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index c329852e..c97a6928 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -636,6 +636,10 @@ some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: decorators that want to customize the `OPTIONS` response on a per-view basis. +- `required_methods`: if this attribute is set, Flask will always add + these methods when registering a URL rule even if the methods were + explicitly overriden in the ``route()`` call. + Full example:: def index(): diff --git a/flask/app.py b/flask/app.py index 40090ab0..a91b0f60 100644 --- a/flask/app.py +++ b/flask/app.py @@ -915,6 +915,10 @@ class Flask(_PackageBoundObject): # a tuple of only `GET` as default. if methods is None: methods = getattr(view_func, 'methods', None) or ('GET',) + methods = set(methods) + + # Methods that should always be added + required_methods = set(getattr(view_func, 'required_methods', ())) # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. @@ -923,11 +927,14 @@ class Flask(_PackageBoundObject): if provide_automatic_options is None: if 'OPTIONS' not in methods: - methods = tuple(methods) + ('OPTIONS',) provide_automatic_options = True + required_methods.add('OPTIONS') else: provide_automatic_options = False + # Add the required methods now. + methods |= required_methods + # due to a werkzeug bug we need to make sure that the defaults are # None if they are an empty dictionary. This should not be necessary # with Werkzeug 0.7 From 086348e2f2874fb701048d5e1390cfe674de1f70 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 8 May 2012 13:14:32 +0100 Subject: [PATCH 1051/3747] Added after_this_request decorator. --- CHANGES | 1 + docs/api.rst | 2 ++ flask/__init__.py | 3 ++- flask/app.py | 2 +- flask/ctx.py | 30 ++++++++++++++++++++++++++++++ flask/testsuite/basic.py | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 2c3f904a..28a0790c 100644 --- a/CHANGES +++ b/CHANGES @@ -70,6 +70,7 @@ Relase date to be decided, codename to be chosen. be used on creation of the :data:`~flask.g` instance of each request. - Added `required_methods` attribute to view functions to force-add methods on registration. +- Added :func:`flask.after_this_request`. Version 0.8.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index c97a6928..dcb54baf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -289,6 +289,8 @@ Useful Functions and Classes .. autofunction:: make_response +.. autofunction:: after_this_request + .. autofunction:: send_file .. autofunction:: send_from_directory diff --git a/flask/__init__.py b/flask/__init__.py index b91f9395..de84bb69 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -25,7 +25,8 @@ from .helpers import url_for, jsonify, json_available, flash, \ get_template_attribute, make_response, safe_join from .globals import current_app, g, request, session, _request_ctx_stack, \ _app_ctx_stack -from .ctx import has_request_context, has_app_context +from .ctx import has_request_context, has_app_context, \ + after_this_request from .module import Module from .blueprints import Blueprint from .templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index a91b0f60..9254c398 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1555,7 +1555,7 @@ class Flask(_PackageBoundObject): """ ctx = _request_ctx_stack.top bp = ctx.request.blueprint - funcs = () + funcs = ctx._after_request_functions if bp is not None and bp in self.after_request_funcs: funcs = reversed(self.after_request_funcs[bp]) if None in self.after_request_funcs: diff --git a/flask/ctx.py b/flask/ctx.py index cf197d05..52eddcb2 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -11,6 +11,7 @@ import sys +from functools import partial from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack, _app_ctx_stack @@ -30,6 +31,31 @@ def _push_app_if_necessary(app): return ctx +def after_this_request(f): + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + _request_ctx_stack.top._after_request_functions.append(f) + return f + + def has_request_context(): """If you have code that wants to test if a request context is there or not this function can be used. For instance, you may want to take advantage @@ -153,6 +179,10 @@ class RequestContext(object): # context, it will be stored there self._pushed_application_context = None + # Functions that should be executed after the request on the response + # object. These will even be called in case of an error. + self._after_request_functions = [] + self.match_request() # XXX: Support for deprecated functionality. This is going away with diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 55b66f78..ba6c2705 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -411,6 +411,21 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_('after' in evts) self.assert_equal(rv, 'request|after') + def test_after_request_processing(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + @flask.after_this_request + def foo(response): + response.headers['X-Foo'] = 'a header' + return response + return 'Test' + c = app.test_client() + resp = c.get('/') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.headers['X-Foo'], 'a header') + def test_teardown_request_handler(self): called = [] app = flask.Flask(__name__) From 444698d42b6cd2bb356d3ac6a7fa9f2be7e0df55 Mon Sep 17 00:00:00 2001 From: Alex Vykalyuk <alekzvik@gmail.com> Date: Mon, 14 May 2012 23:39:27 +0300 Subject: [PATCH 1052/3747] Changed docstring according to docs. --- scripts/flaskext_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index 2f58ccc4..cb0b436c 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -9,6 +9,7 @@ Usage:: import flaskext_compat + flaskext_compat.activate() from flask.ext import foo :copyright: (c) 2011 by Armin Ronacher. From 447afc3525b009ed369943e13437d98e07898bdc Mon Sep 17 00:00:00 2001 From: Marc Abramowitz <marc@marc-abramowitz.com> Date: Sun, 27 May 2012 18:02:54 -0700 Subject: [PATCH 1053/3747] Fix failing test: "AssertionError: 'application/javascript' != 'application/json'" in flask/testsuite/helpers.py", line 88 --- flask/helpers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index e633e1b9..72a961a8 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -133,13 +133,17 @@ def jsonify(*args, **kwargs): """ if __debug__: _assert_have_json() + + padded = kwargs.get('padded', False) if 'padded' in kwargs: - if isinstance(kwargs['padded'], str): - callback = request.args.get(kwargs['padded']) or 'jsonp' + del kwargs['padded'] + + if padded: + if isinstance(padded, str): + callback = request.args.get(padded) or 'jsonp' else: callback = request.args.get('callback') or \ request.args.get('jsonp') or 'jsonp' - del kwargs['padded'] json_str = json.dumps(dict(*args, **kwargs), indent=None) content = str(callback) + "(" + json_str + ")" return current_app.response_class(content, mimetype='application/javascript') From 2c8cbeb0c0b577f8588aa832c4bb763b8e5b3b82 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz <marc@marc-abramowitz.com> Date: Sun, 27 May 2012 18:31:07 -0700 Subject: [PATCH 1054/3747] Add .travis.yml --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d02cae02 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python + +python: + - 2.5 + - 2.6 + - 2.7 + - pypy + +before_install: pip install simplejson + +script: python setup.py test From 99aaacb1a99891aa2eb57f6fe9b8a0c3f87cb454 Mon Sep 17 00:00:00 2001 From: Natan L <kuyanatan.nlao@gmail.com> Date: Wed, 30 May 2012 20:23:02 -0700 Subject: [PATCH 1055/3747] Emended extensiondev.rst. --- docs/extensiondev.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 59ca76c5..d266e1a2 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -47,10 +47,10 @@ registered on PyPI. Also the development checkout link should work so that people can easily install the development version into their virtualenv without having to download the library by hand. -Flask extensions must be licensed as BSD or MIT or a more liberal license -to be enlisted on the Flask Extension Registry. Keep in mind that the -Flask Extension Registry is a moderated place and libraries will be -reviewed upfront if they behave as required. +Flask extensions must be licensed under a BSD, MIT or more liberal license +to be able to be enlisted in the Flask Extension Registry. Keep in mind +that the Flask Extension Registry is a moderated place and libraries will +be reviewed upfront if they behave as required. "Hello Flaskext!" ----------------- From 5c2aa7a9210acb4b16b86d5cda5264a2f5d11aa2 Mon Sep 17 00:00:00 2001 From: Ben Rousch <brousch@gmail.com> Date: Tue, 12 Jun 2012 14:26:10 -0300 Subject: [PATCH 1056/3747] Added link to extensions in "Hook. Extend." section. --- docs/becomingbig.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index ca803060..8d531620 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -25,8 +25,9 @@ The :ref:`api` docs are full of available overrides, hook points, and response objects. Dig deeper on the APIs you use, and look for the customizations which are available out of the box in a Flask release. Look for ways in which your project can be refactored into a collection of utilities and -Flask extensions. Explore the many extensions in the community, and look for -patterns to build your own extensions if you do not find the tools you need. +Flask extensions. Explore the many `extensions +<http://flask.pocoo.org/extensions/>` in the community, and look for patterns to +build your own extensions if you do not find the tools you need. Subclass. --------- From 4b21e2d38ccf66f236c5f1c8a10bd9d7904f2599 Mon Sep 17 00:00:00 2001 From: Massimo Santini <santini@dsi.unimi.it> Date: Wed, 13 Jun 2012 16:43:34 +0300 Subject: [PATCH 1057/3747] I think it should check that cache_timeout is not None to allow for a (I hope legale) value of 0 for such parameter. --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 72a961a8..18502a53 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -533,7 +533,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, rv.cache_control.public = True if cache_timeout is None: cache_timeout = current_app.get_send_file_max_age(filename) - if cache_timeout: + if cache_timeout is not None: rv.cache_control.max_age = cache_timeout rv.expires = int(time() + cache_timeout) From 5bbf8bdcd9619fa15a3380f99fe6d66d08b1f783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramiro=20G=C3=B3mez?= <web@ramiro.org> Date: Sun, 17 Jun 2012 02:22:02 +0300 Subject: [PATCH 1058/3747] Update master --- docs/deploying/fastcgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index b2801560..0e2f6cdc 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -102,7 +102,7 @@ Set yourapplication.fcgi:: def __call__(self, environ, start_response): environ['SCRIPT_NAME'] = '' - return self.app(environ, start_response) + return self.app(environ, start_response) app = ScriptNameStripper(app) From 6809ffccf2b8b94633717f839b42ae8e3fbca1c3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 17 Jun 2012 14:13:53 +0100 Subject: [PATCH 1059/3747] Don't build websites with travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index d02cae02..8cd434d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,3 +9,7 @@ python: before_install: pip install simplejson script: python setup.py test + +branches: + except: + - website From b04827283ea36cc456367f1ac4e0700b90b71283 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 17 Jun 2012 14:17:22 +0100 Subject: [PATCH 1060/3747] Removed padded JSON (JSONP) again. The implementation was not clean and generally the needs for padded json are disappearing now that all browsers support cross site communication with the regular xmlhttprequest. --- flask/helpers.py | 26 -------------------------- flask/testsuite/helpers.py | 19 +------------------ 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 18502a53..71bc3142 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -118,35 +118,9 @@ def jsonify(*args, **kwargs): information about this, have a look at :ref:`json-security`. .. versionadded:: 0.2 - - .. versionadded:: 0.9 - If the ``padded`` argument is true, the JSON object will be padded - for JSONP calls and the response mimetype will be changed to - ``application/javascript``. By default, the request arguments ``callback`` - and ``jsonp`` will be used as the name for the callback function. - This will work with jQuery and most other JavaScript libraries - by default. - - If the ``padded`` argument is a string, jsonify will look for - the request argument with the same name and use that value as the - callback-function name. """ if __debug__: _assert_have_json() - - padded = kwargs.get('padded', False) - if 'padded' in kwargs: - del kwargs['padded'] - - if padded: - if isinstance(padded, str): - callback = request.args.get(padded) or 'jsonp' - else: - callback = request.args.get('callback') or \ - request.args.get('jsonp') or 'jsonp' - json_str = json.dumps(dict(*args, **kwargs), indent=None) - content = str(callback) + "(" + json_str + ")" - return current_app.response_class(content, mimetype='application/javascript') return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index a0e60aac..816f6cd8 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -73,28 +73,11 @@ class JSONTestCase(FlaskTestCase): @app.route('/dict') def return_dict(): return flask.jsonify(d) - @app.route("/unpadded") - def return_padded_false(): - return flask.jsonify(d, padded=False) - @app.route("/padded") - def return_padded_true(): - return flask.jsonify(d, padded=True) - @app.route("/padded_custom") - def return_padded_json_custom_callback(): - return flask.jsonify(d, padded='my_func_name') c = app.test_client() - for url in '/kw', '/dict', '/unpadded': + for url in '/kw', '/dict': rv = c.get(url) self.assert_equal(rv.mimetype, 'application/json') self.assert_equal(flask.json.loads(rv.data), d) - for get_arg in 'callback=funcName', 'jsonp=funcName': - rv = c.get('/padded?' + get_arg) - self.assert_( rv.data.startswith("funcName(") ) - self.assert_( rv.data.endswith(")") ) - rv_json = rv.data.split('(')[1].split(')')[0] - self.assert_equal(flask.json.loads(rv_json), d) - rv = c.get('/padded_custom?my_func_name=funcName') - self.assert_( rv.data.startswith("funcName(") ) def test_json_attr(self): app = flask.Flask(__name__) From 7b1c8fd15b619aee9eb6bc53dd50bd2d7e9bea3d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 17 Jun 2012 14:22:15 +0100 Subject: [PATCH 1061/3747] Added #522 in modified version --- docs/deploying/cgi.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index a2fba90d..1de9bd2c 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -35,12 +35,23 @@ Usually there are two ways to configure the server. Either just copy the `.cgi` into a `cgi-bin` (and use `mod_rewrite` or something similar to rewrite the URL) or let the server point to the file directly. -In Apache for example you can put a like like this into the config: +In Apache for example you can put something like this into the config: .. sourcecode:: apache ScriptAlias /app /path/to/the/application.cgi +On shared webhosting, though, you might not have access to your Apache config. +In this case, a file called `.htaccess`, sitting in the public directory you want +your app to be available, works too but the `ScriptAlias` directive won't +work in that case: + +.. sourcecode:: apache + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f # Don't interfere with static files + RewriteRule ^(.*)$ /path/to/the/application.cgi/$1 [L] + For more information consult the documentation of your webserver. .. _App Engine: http://code.google.com/appengine/ From 1f3e667b5d9ffb60c218c250df27144793a5acdb Mon Sep 17 00:00:00 2001 From: Matt Wright <mdw1980@gmail.com> Date: Mon, 18 Jun 2012 18:33:17 -0300 Subject: [PATCH 1062/3747] Fix documention for `after_this_request` --- flask/ctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 52eddcb2..f64ab04a 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -41,7 +41,7 @@ def after_this_request(f): @app.route('/') def index(): @after_this_request - def add_header(): + def add_header(response): response.headers['X-Foo'] = 'Parachute' return response return 'Hello World!' From 1f82d02b33dad8ac7a4aa49e690767027690fe1a Mon Sep 17 00:00:00 2001 From: bev-a-tron <beverly.a.lau@gmail.com> Date: Mon, 25 Jun 2012 13:31:11 -0400 Subject: [PATCH 1063/3747] Fixes #519 by adding return statement --- docs/quickstart.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e8b71ca9..53ef38d4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -514,8 +514,9 @@ attributes mentioned above:: return log_the_user_in(request.form['username']) else: error = 'Invalid username/password' - # this is executed if the request method was GET or the - # credentials were invalid + # the code below this is executed if the request method + # was GET or the credentials were invalid + return render_template('login.html', error=error) What happens if the key does not exist in the `form` attribute? In that case a special :exc:`KeyError` is raised. You can catch it like a From 8071f11328ab2b767608c5c63d0bad72d9408120 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 26 Jun 2012 17:18:59 +0200 Subject: [PATCH 1064/3747] Fixed an issue with the new path finding logic --- flask/helpers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index e633e1b9..f714a699 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -616,17 +616,29 @@ def get_root_path(import_name): Not to be confused with the package path returned by :func:`find_package`. """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + if mod is not None and hasattr(mod, '__file__'): + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. loader = pkgutil.get_loader(import_name) + + # Loader does not exist or we're referring to an unloaded main module + # or a main module without path (interactive sessions), go with the + # current working directory. if loader is None or import_name == '__main__': - # import name is not found, or interactive/main module return os.getcwd() + # For .egg, zipimporter does not have get_filename until Python 2.7. + # Some other loaders might exhibit the same behavior. if hasattr(loader, 'get_filename'): filepath = loader.get_filename(import_name) else: # Fall back to imports. __import__(import_name) filepath = sys.modules[import_name].__file__ + # filepath is import_name.py for a module, or __init__.py for a package. return os.path.dirname(os.path.abspath(filepath)) From 558750494f6f99b1272218c35d86e6452484e77c Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Wed, 27 Jun 2012 12:08:01 +0100 Subject: [PATCH 1065/3747] Removed unnecessary import --- flask/ctx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 52eddcb2..3f3fbc52 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -11,7 +11,6 @@ import sys -from functools import partial from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack, _app_ctx_stack From 43c6a1ede87da5c20d8b5c7db995cf2d22eb40b2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Wed, 27 Jun 2012 12:22:39 +0100 Subject: [PATCH 1066/3747] Fixed a comment --- flask/ctx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/ctx.py b/flask/ctx.py index 3f3fbc52..90858aa4 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -179,7 +179,8 @@ class RequestContext(object): self._pushed_application_context = None # Functions that should be executed after the request on the response - # object. These will even be called in case of an error. + # object. These will be called before the regular "after_request" + # functions. self._after_request_functions = [] self.match_request() From d5218997d927be869dd55ef04542e1bbc1e69653 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Wed, 27 Jun 2012 15:06:39 +0100 Subject: [PATCH 1067/3747] Added flask.stream_with_context --- CHANGES | 2 + docs/api.rst | 5 +++ docs/patterns/streaming.rst | 23 ++++++++++++ flask/__init__.py | 3 +- flask/ctx.py | 64 +++++++++++++++++++------------- flask/helpers.py | 73 +++++++++++++++++++++++++++++++++++++ flask/testsuite/appctx.py | 20 ++++++++++ flask/testsuite/helpers.py | 59 ++++++++++++++++++++++++++++++ 8 files changed, 222 insertions(+), 27 deletions(-) diff --git a/CHANGES b/CHANGES index 28a0790c..acdfee69 100644 --- a/CHANGES +++ b/CHANGES @@ -71,6 +71,8 @@ Relase date to be decided, codename to be chosen. - Added `required_methods` attribute to view functions to force-add methods on registration. - Added :func:`flask.after_this_request`. +- Added :func:`flask.stream_with_context` and the ability to push contexts + multiple times without producing unexpected behavior. Version 0.8.1 ------------- diff --git a/docs/api.rst b/docs/api.rst index dcb54baf..8a7b5ce0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -375,6 +375,11 @@ Extensions .. versionadded:: 0.8 +Stream Helpers +-------------- + +.. autofunction:: stream_with_context + Useful Internals ---------------- diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index 8393b00b..ac232dcc 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -59,3 +59,26 @@ The template is then evaluated as the stream is iterated over. Since each time you do a yield the server will flush the content to the client you might want to buffer up a few items in the template which you can do with ``rv.enable_buffering(size)``. ``5`` is a sane default. + +Streaming with Context +---------------------- + +.. versionadded:: 0.9 + +Note that when you stream data, the request context is already gone the +moment the function executes. Flask 0.9 provides you with a helper that +can keep the request context around during the execution of the +generator:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + +Without the :func:`~flask.stream_with_context` function you would get a +:class:`RuntimeError` at that point. diff --git a/flask/__init__.py b/flask/__init__.py index de84bb69..e48f7a97 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -22,7 +22,8 @@ from .app import Flask, Request, Response from .config import Config from .helpers import url_for, jsonify, json_available, flash, \ send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute, make_response, safe_join + get_template_attribute, make_response, safe_join, \ + stream_with_context from .globals import current_app, g, request, session, _request_ctx_stack, \ _app_ctx_stack from .ctx import has_request_context, has_app_context, \ diff --git a/flask/ctx.py b/flask/ctx.py index 0cf34491..3ea42a27 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -22,14 +22,6 @@ class _RequestGlobals(object): pass -def _push_app_if_necessary(app): - top = _app_ctx_stack.top - if top is None or top.app != app: - ctx = app.app_context() - ctx.push() - return ctx - - def after_this_request(f): """Executes a function after this request. This is useful to modify response objects. The function is passed the response object and has @@ -110,15 +102,22 @@ class AppContext(object): self.app = app self.url_adapter = app.create_url_adapter(None) + # Like request context, app contexts can be pushed multiple times + # but there a basic "refcount" is enough to track them. + self._refcnt = 0 + def push(self): """Binds the app context to the current context.""" + self._refcnt += 1 _app_ctx_stack.push(self) def pop(self, exc=None): """Pops the app context.""" - if exc is None: - exc = sys.exc_info()[1] - self.app.do_teardown_appcontext(exc) + self._refcnt -= 1 + if self._refcnt <= 0: + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) rv = _app_ctx_stack.pop() assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ % (rv, self) @@ -128,7 +127,7 @@ class AppContext(object): return self def __exit__(self, exc_type, exc_value, tb): - self.pop() + self.pop(exc_value) class RequestContext(object): @@ -169,15 +168,16 @@ class RequestContext(object): self.flashes = None self.session = None + # Request contexts can be pushed multiple times and interleaved with + # other request contexts. Now only if the last level is popped we + # get rid of them. Additionally if an application context is missing + # one is created implicitly so for each level we add this information + self._implicit_app_ctx_stack = [] + # indicator if the context was preserved. Next time another context # is pushed the preserved context is popped. self.preserved = False - # Indicates if pushing this request context also triggered the pushing - # of an application context. If it implicitly pushed an application - # context, it will be stored there - self._pushed_application_context = None - # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. @@ -222,7 +222,13 @@ class RequestContext(object): # Before we push the request context we have to ensure that there # is an application context. - self._pushed_application_context = _push_app_if_necessary(self.app) + app_ctx = _app_ctx_stack.top + if app_ctx is None or app_ctx.app != self.app: + app_ctx = self.app.app_context() + app_ctx.push() + self._implicit_app_ctx_stack.append(app_ctx) + else: + self._implicit_app_ctx_stack.append(None) _request_ctx_stack.push(self) @@ -241,22 +247,28 @@ class RequestContext(object): .. versionchanged:: 0.9 Added the `exc` argument. """ - self.preserved = False - if exc is None: - exc = sys.exc_info()[1] - self.app.do_teardown_request(exc) + app_ctx = self._implicit_app_ctx_stack.pop() + + clear_request = False + if not self._implicit_app_ctx_stack: + self.preserved = False + if exc is None: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + clear_request = True + rv = _request_ctx_stack.pop() assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ % (rv, self) # get rid of circular dependencies at the end of the request # so that we don't require the GC to be active. - rv.request.environ['werkzeug.request'] = None + if clear_request: + rv.request.environ['werkzeug.request'] = None # Get rid of the app as well if necessary. - if self._pushed_application_context: - self._pushed_application_context.pop(exc) - self._pushed_application_context = None + if app_ctx is not None: + app_ctx.pop(exc) def __enter__(self): self.push() diff --git a/flask/helpers.py b/flask/helpers.py index 631e29be..501a2f81 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -21,6 +21,7 @@ from zlib import adler32 from threading import RLock from werkzeug.routing import BuildError from werkzeug.urls import url_quote +from functools import update_wrapper # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -92,6 +93,78 @@ def _endpoint_from_view_func(view_func): return view_func.__name__ +def stream_with_context(generator_or_function): + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) + except TypeError: + def decorator(*args, **kwargs): + gen = generator_or_function() + return stream_with_context(gen) + return update_wrapper(decorator, generator_or_function) + + def generator(): + ctx = _request_ctx_stack.top + if ctx is None: + raise RuntimeError('Attempted to stream with context but ' + 'there was no context in the first place to keep around.') + with ctx: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + for item in gen: + yield item + finally: + if hasattr(gen, 'close'): + gen.close() + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + wrapped_g.next() + return wrapped_g + + def jsonify(*args, **kwargs): """Creates a :class:`~flask.Response` with the JSON representation of the given arguments with an `application/json` mimetype. The arguments diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index 1dcdb406..6454389e 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -75,6 +75,26 @@ class AppContextTestCase(FlaskTestCase): self.assert_equal( flask.render_template_string('{{ g.spam }}'), 'eggs') + def test_context_refcounts(self): + called = [] + app = flask.Flask(__name__) + @app.teardown_request + def teardown_req(error=None): + called.append('request') + @app.teardown_appcontext + def teardown_app(error=None): + called.append('app') + @app.route('/') + def index(): + with flask._app_ctx_stack.top: + with flask._request_ctx_stack.top: + pass + self.assert_(flask._request_ctx_stack.request.environ + ['werkzeug.request'] is not None) + c = app.test_client() + c.get('/') + self.assertEqual(called, ['request', 'app']) + def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 816f6cd8..54c01482 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -397,6 +397,64 @@ class NoImportsTestCase(FlaskTestCase): self.fail('Flask(import_name) is importing import_name.') +class StreamingTestCase(FlaskTestCase): + + def test_streaming_with_context(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + def generate(): + yield 'Hello ' + yield flask.request.args['name'] + yield '!' + return flask.Response(flask.stream_with_context(generate())) + c = app.test_client() + rv = c.get('/?name=World') + self.assertEqual(rv.data, 'Hello World!') + + def test_streaming_with_context_as_decorator(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + @flask.stream_with_context + def generate(): + yield 'Hello ' + yield flask.request.args['name'] + yield '!' + return flask.Response(generate()) + c = app.test_client() + rv = c.get('/?name=World') + self.assertEqual(rv.data, 'Hello World!') + + def test_streaming_with_context_and_custom_close(self): + app = flask.Flask(__name__) + app.testing = True + called = [] + class Wrapper(object): + def __init__(self, gen): + self._gen = gen + def __iter__(self): + return self + def close(self): + called.append(42) + def next(self): + return self._gen.next() + @app.route('/') + def index(): + def generate(): + yield 'Hello ' + yield flask.request.args['name'] + yield '!' + return flask.Response(flask.stream_with_context( + Wrapper(generate()))) + c = app.test_client() + rv = c.get('/?name=World') + self.assertEqual(rv.data, 'Hello World!') + self.assertEqual(called, [42]) + + def suite(): suite = unittest.TestSuite() if flask.json_available: @@ -404,4 +462,5 @@ def suite(): suite.addTest(unittest.makeSuite(SendfileTestCase)) suite.addTest(unittest.makeSuite(LoggingTestCase)) suite.addTest(unittest.makeSuite(NoImportsTestCase)) + suite.addTest(unittest.makeSuite(StreamingTestCase)) return suite From 19def9606ac50bd308ea283e283cbcf62498d6c7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 1 Jul 2012 12:08:38 +0100 Subject: [PATCH 1068/3747] This is 0.8.1 --- CHANGES | 2 +- flask/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 7640ac21..014d579f 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.8.1 ------------- -Bugfix release, release date to be decided +Bugfix release, released on July 1th 2012 - Fixed an issue with the undocumented `flask.session` module to not work properly on Python 2.5. It should not be used but did cause diff --git a/flask/__init__.py b/flask/__init__.py index 04d7d1f2..98fcbe13 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.8.1-dev' +__version__ = '0.8.1' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index af36a8bb..2aad7b57 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.8.1-dev', + version='0.8.1', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From ee3e251f9eb557721517faa6d06a6addd48ebc24 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 1 Jul 2012 12:12:36 +0100 Subject: [PATCH 1069/3747] Updated CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 68ddab78..6d0c4925 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.9 ----------- -Relase date to be decided, codename to be chosen. +Released on July 1st 2012, codename Camapri. - The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted response by default. From d4415dd6653adb25b89b6276dd140141266ba46b Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 1 Jul 2012 12:26:45 +0100 Subject: [PATCH 1070/3747] Fixed an rst syntax error --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..bc40428d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 56f5224ef7cdc48a05b4ce6dcc37043feab0c0bb Mon Sep 17 00:00:00 2001 From: Laurens Van Houtven <_@lvh.cc> Date: Sun, 1 Jul 2012 19:31:53 +0300 Subject: [PATCH 1071/3747] CHANGES: July 1th should be July 1st --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6d0c4925..5889aa92 100644 --- a/CHANGES +++ b/CHANGES @@ -77,7 +77,7 @@ Released on July 1st 2012, codename Camapri. Version 0.8.1 ------------- -Bugfix release, released on July 1th 2012 +Bugfix release, released on July 1st 2012 - Fixed an issue with the undocumented `flask.session` module to not work properly on Python 2.5. It should not be used but did cause From d8e5a37d8a5e9dda1154c6c7c614bb7a4c42afdd Mon Sep 17 00:00:00 2001 From: esaurito <metallourlante@gmail.com> Date: Mon, 2 Jul 2012 00:38:27 +0300 Subject: [PATCH 1072/3747] Fixed codename --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 5889aa92..7d1aeca1 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.9 ----------- -Released on July 1st 2012, codename Camapri. +Released on July 1st 2012, codename Campari. - The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted response by default. From 1beb0f2e7f8c60df41cd7b198c1ed1748a0fea3d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 2 Jul 2012 09:11:24 +0100 Subject: [PATCH 1073/3747] Fixed a syntax error --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..bc40428d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 0553bbdefe55709a1fc36dce445f552f398a428e Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase <sh@lutzhaase.com> Date: Wed, 4 Jul 2012 21:12:41 +0200 Subject: [PATCH 1074/3747] Made it explicitly clear where changes should go in the tutorial --- docs/tutorial/dbinit.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index b546a1a8..15a107cf 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -23,14 +23,17 @@ for you to the application. If you want to do that, you first have to import the :func:`contextlib.closing` function from the contextlib package. If you want to use Python 2.5 it's also necessary to enable the `with` statement -first (`__future__` imports must be the very first import):: +first (`__future__` imports must be the very first import). Accordingly, +the following lines should be to your existing import lines in +`flaskr.py`:: from __future__ import with_statement from contextlib import closing Next we can create a function called `init_db` that initializes the database. For this we can use the `connect_db` function we defined -earlier. Just add that function below the `connect_db` function:: +earlier. Just add that function below the `connect_db` function in +`flask.py`:: def init_db(): with closing(connect_db()) as db: From 690b0c34ff3a9160aad16c613f29b1a8e25ca232 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase <sh@lutzhaase.com> Date: Wed, 4 Jul 2012 21:19:41 +0200 Subject: [PATCH 1075/3747] Fix chose -> choose typo --- docs/tutorial/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index c72bbd7d..1dcc40ed 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -3,7 +3,7 @@ Introducing Flaskr ================== -We will call our blogging application flaskr here, feel free to chose a +We will call our blogging application flaskr here, feel free to choose a less web-2.0-ish name ;) Basically we want it to do the following things: 1. let the user sign in and out with credentials specified in the From ea2a0629c9fc3874319e98b3842365ffd889f43d Mon Sep 17 00:00:00 2001 From: Ron DuPlain <ron.duplain@gmail.com> Date: Wed, 4 Jul 2012 15:27:15 -0400 Subject: [PATCH 1076/3747] Touch up dbinit tutorial doc. Discussion on #pocoo with svenstaro, dAnjou, noob13. --- docs/tutorial/dbinit.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 15a107cf..79479397 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -24,8 +24,7 @@ If you want to do that, you first have to import the :func:`contextlib.closing` function from the contextlib package. If you want to use Python 2.5 it's also necessary to enable the `with` statement first (`__future__` imports must be the very first import). Accordingly, -the following lines should be to your existing import lines in -`flaskr.py`:: +add the following lines to your existing imports in `flaskr.py`:: from __future__ import with_statement from contextlib import closing From c3f651dccbba801fa07426cef1e6604b579b8fe7 Mon Sep 17 00:00:00 2001 From: Simon Sapin <simon.sapin@exyr.org> Date: Thu, 12 Jul 2012 16:31:53 +0300 Subject: [PATCH 1077/3747] Remove the unused `ScriptNameStripper.to_strip` in the FastCGI doc example. Alernatively, `environ['SCRIPT_NAME'] = ''` should be replaced with something like: if environ['SCRIPT_NAME'].startswith(self.to_strip): environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'][len(self.to_strip):] --- docs/deploying/fastcgi.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 0e2f6cdc..8bce7d74 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -95,8 +95,6 @@ Set yourapplication.fcgi:: from yourapplication import app class ScriptNameStripper(object): - to_strip = '/yourapplication.fcgi' - def __init__(self, app): self.app = app From b9df128ba86ec5bc5513bf38b5484f74866ff447 Mon Sep 17 00:00:00 2001 From: Akai Kitsune <open.standards.needed@gmail.com> Date: Mon, 16 Jul 2012 21:08:02 +0300 Subject: [PATCH 1078/3747] Added directions for mod_wsgi vhost configuration under Apache on Windows --- docs/deploying/mod_wsgi.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index c4cd3d61..8fd2c0bb 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -91,6 +91,20 @@ execute the application under a different user for security reasons: </Directory> </VirtualHost> +Note: WSGIDaemonProcess isn't implemented in Windows and Apache will +refuse to run with the above configuration. On a Windows system, eliminate those lines: + +.. sourcecode:: apache + + <VirtualHost *> + ServerName example.com + WSGIScriptAlias / C:\yourdir\yourapp.wsgi + <Directory C:\yourdir> + Order deny,allow + Allow from all + </Directory> + </VirtualHost> + For more information consult the `mod_wsgi wiki`_. .. _mod_wsgi: http://code.google.com/p/modwsgi/ From 40ccc0a99aa9c1f975bcc7f69d452afd46fc8b90 Mon Sep 17 00:00:00 2001 From: Randall Degges <rdegges@gmail.com> Date: Fri, 20 Jul 2012 14:29:10 -0700 Subject: [PATCH 1079/3747] Fixing some wording in the design documentation. --- docs/design.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design.rst b/docs/design.rst index cc247f3b..ee83840e 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -90,9 +90,9 @@ since decorators could be fired in undefined order when the application is split into multiple modules. Another design decision with the Werkzeug routing system is that routes -in Werkzeug try to ensure that there is that URLs are unique. Werkzeug -will go quite far with that in that it will automatically redirect to a -canonical URL if a route is ambiguous. +in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far +with that in that it will automatically redirect to a canonical URL if a route +is ambiguous. One Template Engine From 3800c7396b20d90ff3a84a3f26c9f12e7365a085 Mon Sep 17 00:00:00 2001 From: Ramiro Gomez <web@ramiro.org> Date: Sat, 21 Jul 2012 13:55:45 +0200 Subject: [PATCH 1080/3747] Try to correct confusing sentence in doc and fixed word duplication. --- docs/appcontext.rst | 3 +-- docs/quickstart.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index e9e1ad8f..346cb09b 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -44,8 +44,7 @@ you can have more than one application in the same Python process. So how does the code find the “right” application? In the past we recommended passing applications around explicitly, but that caused issues -with libraries that were not designed with that in mind for libraries for -which it was too inconvenient to make this work. +with libraries that were not designed with that in mind. A common workaround for that problem was to use the :data:`~flask.current_app` proxy later on, which was bound to the current diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e8b71ca9..84ffb488 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -377,7 +377,7 @@ package it's actually inside your package: /hello.html For templates you can use the full power of Jinja2 templates. Head over -to the the official `Jinja2 Template Documentation +to the official `Jinja2 Template Documentation <http://jinja.pocoo.org/2/documentation/templates>`_ for more information. Here is an example template: From b0fdae4e1f45274e0b399523cca9b55627d9afde Mon Sep 17 00:00:00 2001 From: Sven Slootweg <jamsoftgamedev@gmail.com> Date: Sat, 21 Jul 2012 21:00:44 +0200 Subject: [PATCH 1081/3747] Fix regex in lighttpd example config to only match static/ and sub-items, and not all directories that start with 'static' --- docs/deploying/fastcgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 0e2f6cdc..f6ba7cee 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -128,7 +128,7 @@ A basic FastCGI configuration for lighttpd looks like that:: ) url.rewrite-once = ( - "^(/static.*)$" => "$1", + "^(/static($|/.*))$" => "$1", "^(/.*)$" => "/yourapplication.fcgi$1" Remember to enable the FastCGI, alias and rewrite modules. This configuration From 20a542fc8ac6dadbcbac92e84fb554e00ccc897a Mon Sep 17 00:00:00 2001 From: Paul McMillan <paul.mcmillan@nebula.com> Date: Thu, 26 Jul 2012 09:56:01 -0700 Subject: [PATCH 1082/3747] docstring typo --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 8460f476..9cac4025 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1446,7 +1446,7 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.9 This can now also be called without a request object when the - UR adapter is created for the application context. + URL adapter is created for the application context. """ if request is not None: return self.url_map.bind_to_environ(request.environ, From ed1619adadc56641b8ab1ba2c03d45023960aaf2 Mon Sep 17 00:00:00 2001 From: Priit Laes <plaes@plaes.org> Date: Wed, 1 Aug 2012 11:27:28 +0300 Subject: [PATCH 1083/3747] Docs: Mention SERVER_NAME in the url_for() docstring --- flask/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..2e8d0da1 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -305,7 +305,9 @@ def url_for(endpoint, **values): :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule - :param _external: if set to `True`, an absolute URL is generated. + :param _external: if set to `True`, an absolute URL is generated. Server + address can be changed via `SERVER_NAME` configuration variable which + defaults to `localhost`. :param _anchor: if provided this is added as anchor to the URL. :param _method: if provided this explicitly specifies an HTTP method. """ From e3b3e05052ea36456cb557b8799b7b0ed00865bd Mon Sep 17 00:00:00 2001 From: Priit Laes <plaes@plaes.org> Date: Wed, 1 Aug 2012 11:29:40 +0300 Subject: [PATCH 1084/3747] Docs: Fix docstring formatting --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 2e8d0da1..7e20c97d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 4df3bf2058954624f9376fd16774a769299dc40a Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 02:36:14 +0100 Subject: [PATCH 1085/3747] Implemented experimental JSON based sessions --- CHANGES | 9 +++++ docs/api.rst | 7 ++++ docs/upgrading.rst | 51 +++++++++++++++++++++++++ flask/sessions.py | 80 +++++++++++++++++++++++++++++++++++++++- flask/testsuite/basic.py | 26 +++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6d0c4925..1948b5ab 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,15 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.10 +------------ + +Release date to be decided. + +- Changed default cookie serialization format from pickle to JSON to + limit the impact an attacker can do if the secret key leaks. See + :ref:`upgrading-to-010` for more information. + Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 8a7b5ce0..e808e771 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,6 +215,13 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: +.. autoclass:: UpgradeSecureCookieSessionInterface + +.. autoclass:: SecureCookieSession + :members: + +.. autoclass:: UpgradeSecureCookieSession + .. autoclass:: NullSession :members: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7226d60e..01393983 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,57 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +.. _upgrading-to-010: + +Version 0.10 +------------ + +The biggest change going from 0.9 to 0.10 is that the cookie serialization +format changed from pickle to a specialized JSON format. This change has +been done in order to avoid the damage an attacker can do if the secret +key is leaked. When you upgrade you will notice two major changes: all +sessions that were issued before the upgrade are invalidated and you can +only store a limited amount of types in the session. There are two ways +to avoid these problems on upgrading: + +Automatically Upgrade Sessions +`````````````````````````````` + +The first method is to allow pickle based sessions for a limited amount of +time. This can be done by using the +:class:`~flask.sessions.UpgradeSecureCookieSession` session +implementation:: + + from flask import Flask + from flask.sessions import UpgradeSecureCookieSessionInterface + + app = Flask(__name__) + app.session_interface = UpgradeSecureCookieSessionInterface + +For as long as this class is being used both pickle and json sessions are +supported but changes are written in JSON format only. + +Revert to Pickle Sessions +````````````````````````` + +You can also revert to pickle based sessions if you want:: + + import pickle + from flask import Flask + from flask.sessions import SecureCookieSession, \ + SecureCookieSessionInterface + + class PickleSessionInterface(SecureCookieSessionInterface): + class session_class(SecureCookieSession): + serialization_method = pickle + + app = Flask(__name__) + app.session_interface = PickleSessionInterface + +If you want to continue to use pickle based data we strongly recommend +switching to a server side session store however. + + Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index 2795bb1f..75f4a614 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,8 +10,12 @@ :license: BSD, see LICENSE for more details. """ +import cPickle as pickle from datetime import datetime from werkzeug.contrib.securecookie import SecureCookie +from werkzeug.http import http_date, parse_date +from .helpers import json, _assert_have_json +from . import Markup class SessionMixin(object): @@ -41,10 +45,74 @@ class SessionMixin(object): modified = True +class TaggedJSONSerializer(object): + """A customized JSON serializer that supports a few extra types that + we take for granted when serializing (tuples, markup objects, datetime). + """ + + def dumps(self, value): + if __debug__: + _assert_have_json() + def _tag(value): + if isinstance(value, tuple): + return {'##t': [_tag(x) for x in value]} + elif callable(getattr(value, '__html__', None)): + return {'##m': unicode(value.__html__())} + elif isinstance(value, list): + return [_tag(x) for x in value] + elif isinstance(value, datetime): + return {'##d': http_date(value)} + elif isinstance(value, dict): + return dict((k, _tag(v)) for k, v in value.iteritems()) + return value + return json.dumps(_tag(value), separators=(',', ':')) + + def loads(self, value): + if __debug__: + _assert_have_json() + def object_hook(obj): + if len(obj) != 1: + return obj + the_key, the_value = obj.iteritems().next() + if the_key == '##t': + return tuple(the_value) + elif the_key == '##m': + return Markup(the_value) + elif the_key == '##d': + return parse_date(the_value) + return obj + return json.loads(value, object_hook=object_hook) + + +session_json_serializer = TaggedJSONSerializer() + + class SecureCookieSession(SecureCookie, SessionMixin): """Expands the session with support for switching between permanent - and non-permanent sessions. + and non-permanent sessions and changes the default pickle based + serialization format to a tagged json one. """ + serialization_method = session_json_serializer + + +class _UpgradeSerializer(object): + def dumps(self, value): + return session_json_serializer.dumps(value) + def loads(self, value): + try: + return session_json_serializer.loads(value) + except Exception: + return pickle.loads(value) + + +class UpgradeSecureCookieSession(SecureCookieSession): + """This cookie sesion implementation tries json first but will also + support pickle based session. This exists mainly to upgrade existing + pickle based users transparently to json. + + .. versionadded:: 0.10 + """ + serialization_method = _UpgradeSerializer() class NullSession(SecureCookieSession): @@ -203,3 +271,13 @@ class SecureCookieSessionInterface(SessionInterface): session.save_cookie(response, app.session_cookie_name, path=path, expires=expires, httponly=httponly, secure=secure, domain=domain) + + +class UpgradeSecureCookieSessionInterface(SecureCookieSessionInterface): + """This session interface works exactly like the regular one but uses + the :class:`UpgradeSecureCookieSession` classes to upgrade from pickle + sessions to JSON sessions. + + .. versionadded:: 0.10 + """ + session_class = UpgradeSecureCookieSession diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 388b5a8e..3d758b3a 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -13,6 +13,7 @@ from __future__ import with_statement import re import flask +import pickle import unittest from datetime import datetime from threading import Thread @@ -297,6 +298,31 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(c.get('/').data, 'None') self.assert_equal(c.get('/').data, '42') + def test_session_special_types(self): + app = flask.Flask(__name__) + app.secret_key = 'development-key' + app.testing = True + now = datetime.utcnow().replace(microsecond=0) + + @app.after_request + def modify_session(response): + flask.session['m'] = flask.Markup('Hello!') + flask.session['dt'] = now + flask.session['t'] = (1, 2, 3) + return response + + @app.route('/') + def dump_session_contents(): + return pickle.dumps(dict(flask.session)) + + c = app.test_client() + c.get('/') + rv = pickle.loads(c.get('/').data) + self.assert_equal(rv['m'], flask.Markup('Hello!')) + self.assert_equal(type(rv['m']), flask.Markup) + self.assert_equal(rv['dt'], now) + self.assert_equal(rv['t'], (1, 2, 3)) + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From ee28dcf2cff53aae0c88db34ff872e0b042c6bf5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 02:36:29 +0100 Subject: [PATCH 1086/3747] Added changelog entry for 0.10 --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index 6d0c4925..625d22dd 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.10 +------------ + +Release date to be decided. + Version 0.9 ----------- From b87919348104774e9c959fc00976966e38161129 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 02:37:03 +0100 Subject: [PATCH 1087/3747] Set current dev version number to 0.10 --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index b170ba5f..196eb033 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.9' +__version__ = '0.10-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 9b185fc0..1d0761fe 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.9', + version='0.10-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 3f82d1b68ea6f5bf2970c2df8ff5cf991439a9bf Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 03:09:14 +0100 Subject: [PATCH 1088/3747] Switch to itsdangerous --- flask/sessions.py | 100 +++++++++++++++++++--------------------------- setup.py | 3 +- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 75f4a614..3db1fb60 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,13 +10,14 @@ :license: BSD, see LICENSE for more details. """ -import cPickle as pickle from datetime import datetime -from werkzeug.contrib.securecookie import SecureCookie from werkzeug.http import http_date, parse_date -from .helpers import json, _assert_have_json +from werkzeug.datastructures import CallbackDict +from .helpers import json from . import Markup +from itsdangerous import URLSafeTimedSerializer, BadSignature + class SessionMixin(object): """Expands a basic dictionary with an accessors that are expected @@ -51,8 +52,6 @@ class TaggedJSONSerializer(object): """ def dumps(self, value): - if __debug__: - _assert_have_json() def _tag(value): if isinstance(value, tuple): return {'##t': [_tag(x) for x in value]} @@ -68,8 +67,6 @@ class TaggedJSONSerializer(object): return json.dumps(_tag(value), separators=(',', ':')) def loads(self, value): - if __debug__: - _assert_have_json() def object_hook(obj): if len(obj) != 1: return obj @@ -87,32 +84,14 @@ class TaggedJSONSerializer(object): session_json_serializer = TaggedJSONSerializer() -class SecureCookieSession(SecureCookie, SessionMixin): - """Expands the session with support for switching between permanent - and non-permanent sessions and changes the default pickle based - serialization format to a tagged json one. - """ - serialization_method = session_json_serializer +class SecureCookieSession(CallbackDict, SessionMixin): + """Baseclass for sessions based on signed cookies.""" - -class _UpgradeSerializer(object): - def dumps(self, value): - return session_json_serializer.dumps(value) - def loads(self, value): - try: - return session_json_serializer.loads(value) - except Exception: - return pickle.loads(value) - - -class UpgradeSecureCookieSession(SecureCookieSession): - """This cookie sesion implementation tries json first but will also - support pickle based session. This exists mainly to upgrade existing - pickle based users transparently to json. - - .. versionadded:: 0.10 - """ - serialization_method = _UpgradeSerializer() + def __init__(self, initial=None): + def on_update(self): + self.modified = True + CallbackDict.__init__(self, initial, on_update) + self.modified = False class NullSession(SecureCookieSession): @@ -246,38 +225,43 @@ class SessionInterface(object): class SecureCookieSessionInterface(SessionInterface): - """The cookie session interface that uses the Werkzeug securecookie - as client side session backend. - """ + salt = 'cookie-session' session_class = SecureCookieSession + serializer = session_json_serializer + + def get_serializer(self, app): + if not app.secret_key: + return None + return URLSafeTimedSerializer(app.secret_key, + salt=self.salt, + serializer=self.serializer) def open_session(self, app, request): - key = app.secret_key - if key is not None: - return self.session_class.load_cookie(request, - app.session_cookie_name, - secret_key=key) + s = self.get_serializer(app) + if s is None: + return None + val = request.cookies.get(app.session_cookie_name) + if not val: + return self.session_class() + max_age = app.permanent_session_lifetime.total_seconds() + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() def save_session(self, app, session, response): - expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) - if session.modified and not session: - response.delete_cookie(app.session_cookie_name, path=path, - domain=domain) - else: - session.save_cookie(response, app.session_cookie_name, path=path, - expires=expires, httponly=httponly, - secure=secure, domain=domain) - - -class UpgradeSecureCookieSessionInterface(SecureCookieSessionInterface): - """This session interface works exactly like the regular one but uses - the :class:`UpgradeSecureCookieSession` classes to upgrade from pickle - sessions to JSON sessions. - - .. versionadded:: 0.10 - """ - session_class = UpgradeSecureCookieSession + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain, path=path) + return + expires = self.get_expiration_time(app, session) + val = self.get_serializer(app).dumps(dict(session)) + response.set_cookie(app.session_cookie_name, val, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure) diff --git a/setup.py b/setup.py index 1d0761fe..0225e5fc 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,8 @@ setup( platforms='any', install_requires=[ 'Werkzeug>=0.7', - 'Jinja2>=2.4' + 'Jinja2>=2.4', + 'itsdangerous>=0.16' ], classifiers=[ 'Development Status :: 4 - Beta', From c3d38a21c664440fb8284aaaf8fbcce4a1c0849f Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 03:11:40 +0100 Subject: [PATCH 1089/3747] Removed json_available hack --- flask/__init__.py | 6 +++--- flask/helpers.py | 31 ++++--------------------------- flask/wrappers.py | 4 +--- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 196eb033..ac1a3a00 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -20,7 +20,7 @@ from jinja2 import Markup, escape from .app import Flask, Request, Response from .config import Config -from .helpers import url_for, jsonify, json_available, flash, \ +from .helpers import url_for, jsonify, flash, \ send_file, send_from_directory, get_flashed_messages, \ get_template_attribute, make_response, safe_join, \ stream_with_context @@ -37,8 +37,8 @@ from .signals import signals_available, template_rendered, request_started, \ request_finished, got_request_exception, request_tearing_down # only import json if it's available -if json_available: - from .helpers import json +from .helpers import json # backwards compat, goes away in 1.0 from .sessions import SecureCookieSession as Session +json_available = True diff --git a/flask/helpers.py b/flask/helpers.py index 7e20c97d..b2d34a71 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -23,21 +23,9 @@ from werkzeug.routing import BuildError from werkzeug.urls import url_quote from functools import update_wrapper -# try to load the best simplejson implementation available. If JSON -# is not installed, we add a failing class. -json_available = True -json = None -try: - import simplejson as json -except ImportError: - try: - import json - except ImportError: - try: - # Google Appengine offers simplejson via django - from django.utils import simplejson as json - except ImportError: - json_available = False +# Use the same json implementation as itsdangerous on which we +# depend anyways. +from itsdangerous import simplejson as json from werkzeug.datastructures import Headers @@ -55,19 +43,10 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request -def _assert_have_json(): - """Helper function that fails if JSON is unavailable.""" - if not json_available: - raise RuntimeError('simplejson not installed') - - # figure out if simplejson escapes slashes. This behavior was changed # from one version to another without reason. -if not json_available or '\\/' not in json.dumps('/'): - +if '\\/' not in json.dumps('/'): def _tojson_filter(*args, **kwargs): - if __debug__: - _assert_have_json() return json.dumps(*args, **kwargs).replace('/', '\\/') else: _tojson_filter = json.dumps @@ -192,8 +171,6 @@ def jsonify(*args, **kwargs): .. versionadded:: 0.2 """ - if __debug__: - _assert_have_json() return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') diff --git a/flask/wrappers.py b/flask/wrappers.py index 3ee718ff..060ee25b 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -14,7 +14,7 @@ from werkzeug.utils import cached_property from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict -from .helpers import json, _assert_have_json +from .helpers import json from .globals import _request_ctx_stack @@ -95,8 +95,6 @@ class Request(RequestBase): This requires Python 2.6 or an installed version of simplejson. """ - if __debug__: - _assert_have_json() if self.mimetype == 'application/json': request_charset = self.mimetype_params.get('charset') try: From a4977cfe2b57218579ca224af7cfea0864e6665b Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 03:13:16 +0100 Subject: [PATCH 1090/3747] Removed outdated section in the docs --- docs/api.rst | 4 ---- docs/upgrading.rst | 41 ++--------------------------------------- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e808e771..316c76ab 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,13 +215,9 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: -.. autoclass:: UpgradeSecureCookieSessionInterface - .. autoclass:: SecureCookieSession :members: -.. autoclass:: UpgradeSecureCookieSession - .. autoclass:: NullSession :members: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 01393983..c295fb1c 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -29,46 +29,9 @@ format changed from pickle to a specialized JSON format. This change has been done in order to avoid the damage an attacker can do if the secret key is leaked. When you upgrade you will notice two major changes: all sessions that were issued before the upgrade are invalidated and you can -only store a limited amount of types in the session. There are two ways -to avoid these problems on upgrading: - -Automatically Upgrade Sessions -`````````````````````````````` - -The first method is to allow pickle based sessions for a limited amount of -time. This can be done by using the -:class:`~flask.sessions.UpgradeSecureCookieSession` session -implementation:: - - from flask import Flask - from flask.sessions import UpgradeSecureCookieSessionInterface - - app = Flask(__name__) - app.session_interface = UpgradeSecureCookieSessionInterface - -For as long as this class is being used both pickle and json sessions are -supported but changes are written in JSON format only. - -Revert to Pickle Sessions -````````````````````````` - -You can also revert to pickle based sessions if you want:: - - import pickle - from flask import Flask - from flask.sessions import SecureCookieSession, \ - SecureCookieSessionInterface - - class PickleSessionInterface(SecureCookieSessionInterface): - class session_class(SecureCookieSession): - serialization_method = pickle - - app = Flask(__name__) - app.session_interface = PickleSessionInterface - -If you want to continue to use pickle based data we strongly recommend -switching to a server side session store however. +only store a limited amount of types in the session. +TODO: add external module for session upgrading Version 0.9 ----------- From fe85970665ea3a38f9c6a8ef4756ff3a913850b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sat, 11 Aug 2012 03:38:46 +0100 Subject: [PATCH 1091/3747] Various improvements in regards to the itsdangerous usage, bumped to 0.17 --- flask/sessions.py | 55 ++++++++++++++++++++++++++++++++++------------- setup.py | 2 +- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 3db1fb60..ba0b0ed7 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for more details. """ +import hashlib from datetime import datetime from werkzeug.http import http_date, parse_date from werkzeug.datastructures import CallbackDict @@ -54,13 +55,13 @@ class TaggedJSONSerializer(object): def dumps(self, value): def _tag(value): if isinstance(value, tuple): - return {'##t': [_tag(x) for x in value]} + return {' t': [_tag(x) for x in value]} elif callable(getattr(value, '__html__', None)): - return {'##m': unicode(value.__html__())} + return {' m': unicode(value.__html__())} elif isinstance(value, list): return [_tag(x) for x in value] elif isinstance(value, datetime): - return {'##d': http_date(value)} + return {' d': http_date(value)} elif isinstance(value, dict): return dict((k, _tag(v)) for k, v in value.iteritems()) return value @@ -71,11 +72,11 @@ class TaggedJSONSerializer(object): if len(obj) != 1: return obj the_key, the_value = obj.iteritems().next() - if the_key == '##t': + if the_key == ' t': return tuple(the_value) - elif the_key == '##m': + elif the_key == ' m': return Markup(the_value) - elif the_key == '##d': + elif the_key == ' d': return parse_date(the_value) return obj return json.loads(value, object_hook=object_hook) @@ -145,6 +146,13 @@ class SessionInterface(object): #: this type. null_session_class = NullSession + #: A flag that indicates if the session interface is pickle based. + #: This can be used by flask extensions to make a decision in regards + #: to how to deal with the session object. + #: + #: .. versionadded:: 0.10 + pickle_based = False + def make_null_session(self, app): """Creates a null session which acts as a replacement object if the real session support could not be loaded due to a configuration @@ -225,19 +233,36 @@ class SessionInterface(object): class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. salt = 'cookie-session' - session_class = SecureCookieSession + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(hashlib.sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = 'hmac' + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. serializer = session_json_serializer + session_class = SecureCookieSession - def get_serializer(self, app): + def get_signing_serializer(self, app): if not app.secret_key: return None - return URLSafeTimedSerializer(app.secret_key, - salt=self.salt, - serializer=self.serializer) + signer_kwargs = dict( + key_derivation=self.key_derivation, + digest_method=self.digest_method + ) + return URLSafeTimedSerializer(app.secret_key, salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs) def open_session(self, app, request): - s = self.get_serializer(app) + s = self.get_signing_serializer(app) if s is None: return None val = request.cookies.get(app.session_cookie_name) @@ -253,15 +278,15 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) - httponly = self.get_cookie_httponly(app) - secure = self.get_cookie_secure(app) if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain, path=path) return + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) - val = self.get_serializer(app).dumps(dict(session)) + val = self.get_signing_serializer(app).dumps(dict(session)) response.set_cookie(app.session_cookie_name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure) diff --git a/setup.py b/setup.py index 0225e5fc..4a9182b9 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ setup( install_requires=[ 'Werkzeug>=0.7', 'Jinja2>=2.4', - 'itsdangerous>=0.16' + 'itsdangerous>=0.17' ], classifiers=[ 'Development Status :: 4 - Beta', From 77c2c3b183cf54f4cd678ccb4545246ea33fc52b Mon Sep 17 00:00:00 2001 From: Ben Rousch <brousch@gmail.com> Date: Fri, 17 Aug 2012 16:45:04 -0300 Subject: [PATCH 1092/3747] Added _ to fix link to extensions in Hook. Extend. I missed the trailing _ to make a link in my first patch to add the link to extensions. Sorry about that. --- docs/becomingbig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 8d531620..62f456bd 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -26,7 +26,7 @@ response objects. Dig deeper on the APIs you use, and look for the customizations which are available out of the box in a Flask release. Look for ways in which your project can be refactored into a collection of utilities and Flask extensions. Explore the many `extensions -<http://flask.pocoo.org/extensions/>` in the community, and look for patterns to +<http://flask.pocoo.org/extensions/>`_ in the community, and look for patterns to build your own extensions if you do not find the tools you need. Subclass. From 11c746189dd8b49740a833d0e8428a85b6174799 Mon Sep 17 00:00:00 2001 From: Alex Morega <alex@grep.ro> Date: Mon, 20 Aug 2012 16:57:18 +0300 Subject: [PATCH 1093/3747] Fix code example in pluggable views documentation --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index 02c62704..210d8f18 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -36,7 +36,7 @@ based view you would do this:: users = User.query.all() return render_template('users.html', objects=users) - app.add_url_rule('/users/', ShowUsers.as_view('show_users')) + app.add_url_rule('/users/', view_func=ShowUsers.as_view('show_users')) As you can see what you have to do is to create a subclass of :class:`flask.views.View` and implement From 8d330a368a7f0aa6ba8508cb2b4b64f6415a0c7b Mon Sep 17 00:00:00 2001 From: d1ffuz0r <d1fffuz0r@gmail.com> Date: Wed, 29 Aug 2012 22:26:39 +0400 Subject: [PATCH 1094/3747] fixed issue #524 --- scripts/flaskext_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index cb0b436c..050fd7e0 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -44,7 +44,7 @@ class ExtensionImporter(object): def install(self): sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] - def find_module(self, fullname, path=None): + def find_module(self, fullname): if fullname.startswith(self.prefix): return self From 69e419e020ea731a5bf4d4e95a6227761f01bcc7 Mon Sep 17 00:00:00 2001 From: pinchsp <spappier@gmail.com> Date: Tue, 4 Sep 2012 00:51:45 -0300 Subject: [PATCH 1095/3747] Fixed small mistake in sqlalchemy pattern --- docs/patterns/sqlalchemy.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 270fa79e..c6200187 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -42,7 +42,7 @@ Here the example `database.py` module for your application:: engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) Base = declarative_base() Base.query = db_session.query_property() @@ -130,7 +130,7 @@ Here is an example `database.py` module for your application:: metadata = MetaData() db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) def init_db(): metadata.create_all(bind=engine) @@ -189,7 +189,7 @@ To insert data you can use the `insert` method. We have to get a connection first so that we can use a transaction: >>> con = engine.connect() ->>> con.execute(users.insert(name='admin', email='admin@localhost')) +>>> con.execute(users.insert(), name='admin', email='admin@localhost') SQLAlchemy will automatically commit for us. From 48f7cdd01699fdc4048b18d827fccfbf9f266c9f Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan <finbarr.ocallaghan@gmail.com> Date: Thu, 6 Sep 2012 18:04:51 +0100 Subject: [PATCH 1096/3747] various typo fixes --- docs/appcontext.rst | 4 ++-- docs/upgrading.rst | 2 +- flask/app.py | 18 +++++++++--------- flask/exthook.py | 2 +- flask/testsuite/subclassing.py | 8 ++++---- flask/wrappers.py | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 346cb09b..928ffc8a 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -37,7 +37,7 @@ context local. Purpose of the Application Context ---------------------------------- -The main reason for the application's context existance is that in the +The main reason for the application's context existence is that in the past a bunch of functionality was attached to the request context in lack of a better solution. Since one of the pillar's of Flask's design is that you can have more than one application in the same Python process. @@ -58,7 +58,7 @@ Creating an Application Context To make an application context there are two ways. The first one is the implicit one: whenever a request context is pushed, an application context will be created alongside if this is necessary. As a result of that, you -can ignore the existance of the application context unless you need it. +can ignore the existence of the application context unless you need it. The second way is the explicit way using the :meth:`~flask.Flask.app_context` method:: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7226d60e..f3e3b021 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -191,7 +191,7 @@ Manual Error Handler Attaching While it is still possible to attach error handlers to :attr:`Flask.error_handlers` it's discouraged to do so and in fact -deprecated. In generaly we no longer recommend custom error handler +deprecated. In generally we no longer recommend custom error handler attaching via assignments to the underlying dictionary due to the more complex internal handling to support arbitrary exception classes and blueprints. See :meth:`Flask.errorhandler` for more information. diff --git a/flask/app.py b/flask/app.py index f8526d5c..68767d03 100644 --- a/flask/app.py +++ b/flask/app.py @@ -541,8 +541,8 @@ class Flask(_PackageBoundObject): Here some examples:: app.logger.debug('A value for debugging') - app.logger.warning('A warning ocurred (%d apples)', 42) - app.logger.error('An error occoured') + app.logger.warning('A warning occurred (%d apples)', 42) + app.logger.error('An error occurred') .. versionadded:: 0.3 """ @@ -846,7 +846,7 @@ class Flask(_PackageBoundObject): first_registration = False if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ - 'A blueprint\'s name collision ocurred between %r and ' \ + 'A blueprint\'s name collision occurred between %r and ' \ '%r. Both share the same name "%s". Blueprints that ' \ 'are created on the fly need unique names.' % \ (blueprint, self.blueprints[blueprint.name], blueprint.name) @@ -1108,7 +1108,7 @@ class Flask(_PackageBoundObject): a new response object or the same (see :meth:`process_response`). As of Flask 0.7 this function might not be executed at the end of the - request in case an unhandled exception ocurred. + request in case an unhandled exception occurred. """ self.after_request_funcs.setdefault(None, []).append(f) return f @@ -1132,10 +1132,10 @@ class Flask(_PackageBoundObject): stack of active contexts. This becomes relevant if you are using such constructs in tests. - Generally teardown functions must take every necesary step to avoid + Generally teardown functions must take every necessary step to avoid that they will fail. If they do execute code that might fail they will have to surround the execution of these code by try/except - statements and log ocurring errors. + statements and log occurring errors. When a teardown function was called because of a exception it will be passed an error object. @@ -1265,7 +1265,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not caught. In debug mode the exception will + occurs that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. @@ -1522,7 +1522,7 @@ class Flask(_PackageBoundObject): request handling is stopped. This also triggers the :meth:`url_value_processor` functions before - the actualy :meth:`before_request` functions are called. + the actually :meth:`before_request` functions are called. """ bp = _request_ctx_stack.top.request.blueprint @@ -1675,7 +1675,7 @@ class Flask(_PackageBoundObject): The behavior of the before and after request callbacks was changed under error conditions and a new callback was added that will always execute at the end of the request, independent on if an - error ocurred or not. See :ref:`callbacks-and-errors`. + error occurred or not. See :ref:`callbacks-and-errors`. :param environ: a WSGI environment :param start_response: a callable accepting a status code, diff --git a/flask/exthook.py b/flask/exthook.py index bb1deb29..26578f0f 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -110,7 +110,7 @@ class ExtensionImporter(object): if module_name == important_module: return True - # Some python verisons will will clean up modules so early that the + # Some python versions will will clean up modules so early that the # module name at that point is no longer set. Try guessing from # the filename then. filename = os.path.abspath(tb.tb_frame.f_code.co_filename) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index e56ad563..89aa9150 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -18,14 +18,14 @@ from flask.testsuite import FlaskTestCase class FlaskSubclassingTestCase(FlaskTestCase): - def test_supressed_exception_logging(self): - class SupressedFlask(flask.Flask): + def test_suppressed_exception_logging(self): + class SuppressedFlask(flask.Flask): def log_exception(self, exc_info): pass out = StringIO() - app = SupressedFlask(__name__) - app.logger_name = 'flask_tests/test_supressed_exception_logging' + app = SuppressedFlask(__name__) + app.logger_name = 'flask_tests/test_suppressed_exception_logging' app.logger.addHandler(StreamHandler(out)) @app.route('/') diff --git a/flask/wrappers.py b/flask/wrappers.py index 3ee718ff..50dfdc2a 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -108,7 +108,7 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of - this method is used by :attr:`json` when an error ocurred. The default + this method is used by :attr:`json` when an error occurred. The default implementation raises a :class:`JSONBadRequest`, which is a subclass of :class:`~werkzeug.exceptions.BadRequest` which sets the ``Content-Type`` to ``application/json`` and provides a JSON-formatted From e8d50a7abafff3306c5076f10d82b66224c0ebd4 Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan <finbarr.ocallaghan@gmail.com> Date: Thu, 6 Sep 2012 18:19:50 +0100 Subject: [PATCH 1097/3747] fixed spelling but not the grammar! --- docs/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f3e3b021..77c78ae4 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -191,7 +191,7 @@ Manual Error Handler Attaching While it is still possible to attach error handlers to :attr:`Flask.error_handlers` it's discouraged to do so and in fact -deprecated. In generally we no longer recommend custom error handler +deprecated. In general we no longer recommend custom error handler attaching via assignments to the underlying dictionary due to the more complex internal handling to support arbitrary exception classes and blueprints. See :meth:`Flask.errorhandler` for more information. From e93447f25e5f0cf0c10c32daa10b02e23ba2bc1a Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan <finbarr.ocallaghan@gmail.com> Date: Thu, 6 Sep 2012 18:30:41 +0100 Subject: [PATCH 1098/3747] actually to actual, again, fixed spelling, not grammar --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 68767d03..b47e5a69 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1522,7 +1522,7 @@ class Flask(_PackageBoundObject): request handling is stopped. This also triggers the :meth:`url_value_processor` functions before - the actually :meth:`before_request` functions are called. + the actual :meth:`before_request` functions are called. """ bp = _request_ctx_stack.top.request.blueprint From 9ecbd20286aebcd2040cf7463d5288726787c1f3 Mon Sep 17 00:00:00 2001 From: Ralph Bean <rbean@redhat.com> Date: Thu, 13 Sep 2012 15:16:38 -0300 Subject: [PATCH 1099/3747] Update flask/templating.py Fixed a typo in the docstring. --- flask/templating.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index c809a63f..e5d896cb 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -129,8 +129,8 @@ def render_template_string(source, **context): """Renders a template from the given template source string with the given context. - :param template_name: the sourcecode of the template to be - rendered + :param source: the sourcecode of the template to be + rendered :param context: the variables that should be available in the context of the template. """ From 22af78a96f98222c2292d71dd1914f5be56362a0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Fri, 21 Sep 2012 01:02:42 +0900 Subject: [PATCH 1100/3747] Removed outdated sentence in the testing docs --- docs/testing.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 1e00fe80..d4d9259c 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -270,10 +270,6 @@ happen. With Flask 0.4 this is possible by using the If you were to use just the :meth:`~flask.Flask.test_client` without the `with` block, the `assert` would fail with an error because `request` is no longer available (because you are trying to use it outside of the actual request). -However, keep in mind that any :meth:`~flask.Flask.after_request` functions -are already called at this point so your database connection and -everything involved is probably already closed down. - Accessing and Modifying Sessions -------------------------------- From 7233a3e0a223e8d426ff937f5c03d88636b0ade4 Mon Sep 17 00:00:00 2001 From: Ryan Macy <ryan@bitrot.io> Date: Mon, 1 Oct 2012 14:45:02 -0500 Subject: [PATCH 1101/3747] Fixed typo occours to occurs Fixed a typo in the docstring of handle_exception. Was occours, now occurs. --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index f8526d5c..08fc3248 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1265,7 +1265,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not caught. In debug mode the exception will + occurs that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. From 639817b6212465264d9c24faf4e43cefa2e7ae50 Mon Sep 17 00:00:00 2001 From: jfinkels <jeffrey.finkelstein@gmail.com> Date: Fri, 5 Oct 2012 02:49:55 -0300 Subject: [PATCH 1102/3747] Update docs/quickstart.rst Removed incorrect syntax and simplified remaining sentence. --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1d72e510..e9d6b388 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -627,9 +627,9 @@ For this also see :ref:`about-responses`. Redirects and Errors -------------------- -To redirect a user to somewhere else you can use the -:func:`~flask.redirect` function. To abort a request early with an error -code use the :func:`~flask.abort` function. Here an example how this works:: +To redirect a user to another endpoint, use the :func:`~flask.redirect` +function; to abort a request early with an error code, use the +:func:`~flask.abort` function:: from flask import abort, redirect, url_for From 261c4a6aee88361e0de5d86061d873bbad2cb3a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 12:40:59 +0200 Subject: [PATCH 1103/3747] Updated documentation for the new sessions --- docs/upgrading.rst | 9 +++++++-- flask/sessions.py | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index c295fb1c..34f54f42 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -29,9 +29,14 @@ format changed from pickle to a specialized JSON format. This change has been done in order to avoid the damage an attacker can do if the secret key is leaked. When you upgrade you will notice two major changes: all sessions that were issued before the upgrade are invalidated and you can -only store a limited amount of types in the session. +only store a limited amount of types in the session. The new sessions are +by design much more restricted to only allow JSON with a few small +extensions for tuples and strings with HTML markup. -TODO: add external module for session upgrading +In order to not break people's sessions it is possible to continue using +the old session system by using the `Flask-OldSessions_` extension. + +.. _Flask-OldSessions: http://packages.python.org/Flask-OldSessions/ Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index ba0b0ed7..4790f73a 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -3,10 +3,9 @@ flask.sessions ~~~~~~~~~~~~~~ - Implements cookie based sessions based on Werkzeug's secure cookie - system. + Implements cookie based sessions based on itsdangerous. - :copyright: (c) 2011 by Armin Ronacher. + :copyright: (c) 2012 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ From 4f1cb4212376acbbe7ab14fb8caa8a5025b4659e Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 12:48:19 +0200 Subject: [PATCH 1104/3747] make_test_environ_builder when used with subdomains was not working correctly, now it uses urlparse module for detecting full URL and changing path and base_url correctly --- flask/testing.py | 6 +++++- flask/testsuite/testing.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/flask/testing.py b/flask/testing.py index 782b40f6..bdd3860f 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -15,6 +15,7 @@ from __future__ import with_statement from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +from urlparse import urlparse def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): @@ -22,9 +23,12 @@ def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): http_host = app.config.get('SERVER_NAME') app_root = app.config.get('APPLICATION_ROOT') if base_url is None: - base_url = 'http://%s/' % (http_host or 'localhost') + url = urlparse(path) + base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') if app_root: base_url += app_root.lstrip('/') + if url.netloc: + path = url.path return EnvironBuilder(path, base_url, *args, **kwargs) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 0e6feb60..92e3f267 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -198,7 +198,46 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(called, [None, None]) +class SubdomainTestCase(FlaskTestCase): + + def setUp(self): + self.app = flask.Flask(__name__) + self.app.config['SERVER_NAME'] = 'example.com' + self.client = self.app.test_client() + + self._ctx = self.app.test_request_context() + self._ctx.push() + + def tearDown(self): + if self._ctx is not None: + self._ctx.pop() + + def test_subdomain(self): + @self.app.route('/', subdomain='<company_id>') + def view(company_id): + return company_id + + url = flask.url_for('view', company_id='xxx') + response = self.client.get(url) + + self.assertEquals(200, response.status_code) + self.assertEquals('xxx', response.data) + + + def test_nosubdomain(self): + @self.app.route('/<company_id>') + def view(company_id): + return company_id + + url = flask.url_for('view', company_id='xxx') + response = self.client.get(url) + + self.assertEquals(200, response.status_code) + self.assertEquals('xxx', response.data) + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestToolsTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) return suite From f034d8d3451f590fca1badeac5da0230bed9b148 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 12:51:46 +0200 Subject: [PATCH 1105/3747] Add @template_test() decorator for creating custom jinja2 tests, like existing @template_filter() for filters. Fixes #332 --- flask/app.py | 38 ++++++ flask/blueprints.py | 28 +++++ flask/testsuite/blueprints.py | 116 ++++++++++++++++++- flask/testsuite/templates/template_test.html | 3 + flask/testsuite/templating.py | 91 +++++++++++++-- 5 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 flask/testsuite/templates/template_test.html diff --git a/flask/app.py b/flask/app.py index 08fc3248..7940a349 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1086,6 +1086,44 @@ class Flask(_PackageBoundObject): """ self.jinja_env.filters[name or f.__name__] = f + @setupmethod + def template_test(self, name=None): + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in xrange(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def decorator(f): + self.add_template_test(f, name=name) + return f + return decorator + + @setupmethod + def add_template_test(self, f, name=None): + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" diff --git a/flask/blueprints.py b/flask/blueprints.py index 9c557028..7ce23bbc 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -209,6 +209,34 @@ class Blueprint(_PackageBoundObject): state.app.jinja_env.filters[name or f.__name__] = f self.record_once(register_template) + def app_template_test(self, name=None): + """Register a custom template test, available application wide. Like + :meth:`Flask.template_test` but for a blueprint. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def decorator(f): + self.add_app_template_test(f, name=name) + return f + return decorator + + def add_app_template_test(self, f, name=None): + """Register a custom template test, available application wide. Like + :meth:`Flask.add_template_test` but for a blueprint. Works exactly + like the :meth:`app_template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def register_template(state): + state.app.jinja_env.tests[name or f.__name__] = f + self.record_once(register_template) + def before_request(self, f): """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index c9622121..ea047918 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -548,7 +548,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -559,7 +559,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -570,7 +570,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -581,7 +581,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse, 'strrev') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -650,6 +650,114 @@ class BlueprintTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_template_test(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) + self.assert_(app.jinja_env.tests['is_boolean'](False)) + + def test_add_template_test(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) + self.assert_(app.jinja_env.tests['is_boolean'](False)) + + def test_template_test_with_name(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test_with_name(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_after_route_with_template(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + app.register_blueprint(bp, url_prefix='/py') + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_template(self): + bp = flask.Blueprint('bp', __name__) + def boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(boolean) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/templates/template_test.html b/flask/testsuite/templates/template_test.html new file mode 100644 index 00000000..92d5561b --- /dev/null +++ b/flask/testsuite/templates/template_test.html @@ -0,0 +1,3 @@ +{% if value is boolean %} + Success! +{% endif %} diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 4a0ebdbc..1df4292d 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -89,7 +89,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter() def my_reverse(s): return s[::-1] - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -98,7 +98,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse) - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -107,7 +107,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter('strrev') def my_reverse(s): return s[::-1] - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -116,7 +116,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, 'strrev') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -164,6 +164,86 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_template_test(self): + app = flask.Flask(__name__) + @app.template_test() + def boolean(value): + return isinstance(value, bool) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test(self): + app = flask.Flask(__name__) + def boolean(value): + return isinstance(value, bool) + app.add_template_test(boolean) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_name(self): + app = flask.Flask(__name__) + @app.template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test_with_name(self): + app = flask.Flask(__name__) + def is_boolean(value): + return isinstance(value, bool) + app.add_template_test(is_boolean, 'boolean') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_template(self): + app = flask.Flask(__name__) + @app.template_test() + def boolean(value): + return isinstance(value, bool) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_template(self): + app = flask.Flask(__name__) + def boolean(value): + return isinstance(value, bool) + app.add_template_test(boolean) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_name_and_template(self): + app = flask.Flask(__name__) + def is_boolean(value): + return isinstance(value, bool) + app.add_template_test(is_boolean, 'boolean') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + def test_custom_template_loader(self): class MyFlask(flask.Flask): def create_global_jinja_loader(self): @@ -177,7 +257,6 @@ class TemplatingTestCase(FlaskTestCase): rv = c.get('/') self.assert_equal(rv.data, 'Hello Custom World!') - def test_iterable_loader(self): app = flask.Flask(__name__) @app.context_processor @@ -195,8 +274,6 @@ class TemplatingTestCase(FlaskTestCase): self.assert_equal(rv.data, '<h1>Jameson</h1>') - - def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TemplatingTestCase)) From c9a7fdf1b02ee79676d39bb614007bf922931a9c Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 12:53:36 +0200 Subject: [PATCH 1106/3747] Documented latest commit in changelog --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index a7c97cd7..be7ede79 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,8 @@ Release date to be decided. - Changed default cookie serialization format from pickle to JSON to limit the impact an attacker can do if the secret key leaks. See :ref:`upgrading-to-010` for more information. +- Added ``template_test`` methods in addition to the already existing + ``template_filter`` method family. Version 0.9 ----------- From 18413ed1bf08261acf6d40f8ba65a98ae586bb29 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 13:02:05 +0200 Subject: [PATCH 1107/3747] Added HTTP override middleware to docs. This fixes #582 --- docs/patterns/index.rst | 1 + docs/patterns/methodoverrides.rst | 43 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/patterns/methodoverrides.rst diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 964b1e17..2b1eaa9a 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -37,3 +37,4 @@ Snippet Archives <http://flask.pocoo.org/snippets/>`_. favicon streaming deferredcallbacks + methodoverrides diff --git a/docs/patterns/methodoverrides.rst b/docs/patterns/methodoverrides.rst new file mode 100644 index 00000000..d5c187b6 --- /dev/null +++ b/docs/patterns/methodoverrides.rst @@ -0,0 +1,43 @@ +Adding HTTP Method Overrides +============================ + +Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP +methods (such as PATCH). In that case it's possible to “proxy” HTTP +methods through another HTTP method in total violation of the protocol. + +The way this works is by letting the client do an HTTP POST request and +set the ``X-HTTP-Method-Override`` header and set the value to the +intended HTTP method (such as ``PATCH``). + +This can easily be accomplished with an HTTP middleware:: + + class HTTPMethodOverrideMiddleware(object): + allowed_methods = frozenset([ + 'GET', + 'HEAD', + 'POST', + 'DELETE', + 'PUT', + 'PATCH', + 'OPTIONS' + ]) + bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() + if method in self.allowed_methods: + method = method.encode('ascii', 'replace') + environ['REQUEST_METHOD'] = method + if method in self.bodyless_methods: + environ['CONTENT_LENGTH'] = '0' + return self.app(environ, start_response) + +To use this with Flask this is all that is necessary:: + + from flask import Flask + + app = Flask(__name__) + app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) From 5b462dd38224648e7a3e533449784a9e236f957d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 14:50:21 +0200 Subject: [PATCH 1108/3747] Fixed a typo in the docs. This fixes #576 and #575 --- docs/signals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index 959c53bd..5f0af048 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -285,7 +285,7 @@ The following signals exist in Flask: def close_db_connection(sender, **extra): session.close() - from flask import request_tearing_down + from flask import appcontext_tearing_down appcontext_tearing_down.connect(close_db_connection, app) This will also be passed an `exc` keyword argument that has a reference From 7f8709147435bea69fa15ae0de9a2326ab5dfc41 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 14:51:26 +0200 Subject: [PATCH 1109/3747] Added a missing exposed import. This fixes #575 --- flask/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index ac1a3a00..fda94f39 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -34,7 +34,8 @@ from .templating import render_template, render_template_string # the signals from .signals import signals_available, template_rendered, request_started, \ - request_finished, got_request_exception, request_tearing_down + request_finished, got_request_exception, request_tearing_down, \ + appcontext_tearing_down # only import json if it's available from .helpers import json From de5038f2fb25189e6d2ba8bc479d3d940a8ea43b Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 14:56:02 +0200 Subject: [PATCH 1110/3747] Added total_seconds() helper for pythons before 2.7 --- flask/sessions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask/sessions.py b/flask/sessions.py index 4790f73a..343fa94d 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -19,6 +19,10 @@ from . import Markup from itsdangerous import URLSafeTimedSerializer, BadSignature +def total_seconds(td): + return td.days * 60 * 60 * 24 + td.seconds + + class SessionMixin(object): """Expands a basic dictionary with an accessors that are expected by Flask extensions and users for the session. @@ -267,7 +271,7 @@ class SecureCookieSessionInterface(SessionInterface): val = request.cookies.get(app.session_cookie_name) if not val: return self.session_class() - max_age = app.permanent_session_lifetime.total_seconds() + max_age = total_seconds(app.permanent_session_lifetime) try: data = s.loads(val, max_age=max_age) return self.session_class(data) From 3bec75d230507335eaf9066605d3893e185b9e11 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 15:24:03 +0200 Subject: [PATCH 1111/3747] Set the content-length header for sendfile. Fixes #447 --- CHANGES | 1 + flask/helpers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index be7ede79..0c7a8e1e 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Release date to be decided. :ref:`upgrading-to-010` for more information. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. +- Set the content-length header for x-sendfile. Version 0.9 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index b2d34a71..07c8add2 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -541,6 +541,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if file is not None: file.close() headers['X-Sendfile'] = filename + headers['Content-Length'] = os.path.getsize(filename) data = None else: if file is None: From c4f2075f4c4c27856fe0af77250fb75c61c0d86b Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 15:33:25 +0200 Subject: [PATCH 1112/3747] tojson no longer escapes script blocks in HTML5 parsers. Fixed #605 --- CHANGES | 1 + flask/helpers.py | 12 +++++++----- flask/testsuite/helpers.py | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 0c7a8e1e..10a2fe1c 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Release date to be decided. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. - Set the content-length header for x-sendfile. +- ``tojson`` filter now does not escape script blocks in HTML5 parsers. Version 0.9 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index 07c8add2..9491ac55 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -45,11 +45,13 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ # figure out if simplejson escapes slashes. This behavior was changed # from one version to another without reason. -if '\\/' not in json.dumps('/'): - def _tojson_filter(*args, **kwargs): - return json.dumps(*args, **kwargs).replace('/', '\\/') -else: - _tojson_filter = json.dumps +_slash_escape = '\\/' not in json.dumps('/') + +def _tojson_filter(*args, **kwargs): + rv = json.dumps(*args, **kwargs) + if _slash_escape: + rv = rv.replace('/', '\\/') + return rv.replace('<!', '<\\u0021') # sentinel diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 54c01482..58592668 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -97,6 +97,8 @@ class JSONTestCase(FlaskTestCase): self.assert_equal(rv, '"<\\/script>"') rv = render('{{ "<\0/script>"|tojson|safe }}') self.assert_equal(rv, '"<\\u0000\\/script>"') + rv = render('{{ "<!--<script>"|tojson|safe }}') + self.assert_equal(rv, '"<\\u0021--<script>"') def test_modified_url_encoding(self): class ModifiedRequest(flask.Request): From b5bb49d080475e4fb90f6a47dacab60b53470e9d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 15:46:21 +0200 Subject: [PATCH 1113/3747] Added a new example for checksums on input data. This fixes #601 --- docs/patterns/index.rst | 1 + docs/patterns/requestchecksum.rst | 55 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 docs/patterns/requestchecksum.rst diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 2b1eaa9a..4a09340f 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -38,3 +38,4 @@ Snippet Archives <http://flask.pocoo.org/snippets/>`_. streaming deferredcallbacks methodoverrides + requestchecksum diff --git a/docs/patterns/requestchecksum.rst b/docs/patterns/requestchecksum.rst new file mode 100644 index 00000000..902be64a --- /dev/null +++ b/docs/patterns/requestchecksum.rst @@ -0,0 +1,55 @@ +Request Content Checksums +========================= + +Various pieces of code can consume the request data and preprocess it. +For instance JSON data ends up on the request object already read and +processed, form data ends up there as well but goes through a different +code path. This seems inconvenient when you want to calculate the +checksum of the incoming request data. This is necessary sometimes for +some APIs. + +Fortunately this is however very simple to change by wrapping the input +stream. + +The following example calculates the SHA1 checksum of the incoming data as +it gets read and stores it in the WSGI environment:: + + import hashlib + + class ChecksumCalcStream(object): + + def __init__(self, stream): + self._stream = stream + self._hash = hashlib.sha1() + + def read(self, bytes): + rv = self._stream.read(bytes) + self._hash.update(rv) + return rv + + def readline(self, size_hint): + rv = self._stream.readline(size_hint) + self._hash.update(rv) + return rv + + def generate_checksum(request): + env = request.environ + stream = ChecksumCalcStream(env['wsgi.input']) + env['wsgi.input'] = stream + return stream._hash + +To use this, all you need to do is to hook the calculating stream in +before the request starts consuming data. (Eg: be careful accessing +``request.form`` or anything of that nature. ``before_request_handlers`` +for instance should be careful not to access it). + +Example usage:: + + @app.route('/special-api', methods=['POST']) + def special_api(): + hash = generate_checksum(request) + # Accessing this parses the input stream + files = request.files + # At this point the hash is fully constructed. + checksum = hash.hexdigest() + return 'Hash was: %s' % checksum From 3c1d7758d54a5ad2c9fa1ea0933ed10b6cfeb696 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 15:58:21 +0200 Subject: [PATCH 1114/3747] Removed dev tag from setup.cfg. Fixes #596 --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2d74c58f..6116ecd1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,4 @@ [egg_info] -tag_build = dev tag_date = true [aliases] From f8b6033a3b86691470a36db57eba17d996c7d5e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 16:04:31 +0200 Subject: [PATCH 1115/3747] Added a workaround for samefile. This fixes #600 --- flask/testsuite/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 76a4d724..5d86fa3d 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -32,9 +32,12 @@ def add_to_path(path): raise RuntimeError('Tried to add nonexisting path') def _samefile(x, y): + if x == y: + return True try: return os.path.samefile(x, y) - except (IOError, OSError): + except (IOError, OSError, AttributeError): + # Windows has no samefile return False sys.path[:] = [x for x in sys.path if not _samefile(path, x)] sys.path.insert(0, path) From b6f37c40f8d426f4af340b722d28ccea7f4caf2a Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 16:08:00 +0200 Subject: [PATCH 1116/3747] Updated travis config for notifications --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8cd434d1..6d0ab2d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,13 @@ script: python setup.py test branches: except: - website + +notifications: + # Disable travis notifications until they figured out how to hide + # their own builder failure from us. Travis currently fails way + # too many times by itself. + email: false + + irc: + channels: + - "irc.freenode.org#pocoo" From 6e4015d62427a220206d58742ae3e54439ecd97a Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 16:08:13 +0200 Subject: [PATCH 1117/3747] Removed tox file --- Makefile | 3 --- tox.ini | 8 -------- 2 files changed, 11 deletions(-) delete mode 100644 tox.ini diff --git a/Makefile b/Makefile index 08811ac2..773c680e 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,6 @@ audit: release: python scripts/make-release.py -tox-test: - PYTHONDONTWRITEBYTECODE= tox - ext-test: python tests/flaskext_test.py --browse diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 82c1588e..00000000 --- a/tox.ini +++ /dev/null @@ -1,8 +0,0 @@ -[tox] -envlist=py25,py26,py27,pypy - -[testenv] -commands=python run-tests.py - -[testenv:py25] -deps=simplejson From e2b3f07d7c94793414693a310d54c667e0a8da7d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 16:16:21 +0200 Subject: [PATCH 1118/3747] Stop the joinspam --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6d0ab2d6..fd590bea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,3 +23,5 @@ notifications: irc: channels: - "irc.freenode.org#pocoo" + use_notice: true + skip_join: true From 661ee54bc2bc1ea0763ac9c226f8e14bb0beb5b1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 17:12:16 +0200 Subject: [PATCH 1119/3747] Raise exceptions if a function is overridden by a new endpoint. This fixes #570 --- flask/app.py | 5 +++++ flask/testsuite/basic.py | 2 +- flask/testsuite/views.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 91db1438..327cb8cf 100644 --- a/flask/app.py +++ b/flask/app.py @@ -942,8 +942,13 @@ class Flask(_PackageBoundObject): rule = self.url_rule_class(rule, methods=methods, **options) rule.provide_automatic_options = provide_automatic_options + self.url_map.add(rule) if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func is not view_func: + raise AssertionError('View function mapping is overwriting an ' + 'existing endpoint function: %s' % endpoint) self.view_functions[endpoint] = view_func def route(self, rule, **options): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 3d758b3a..964d2c18 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -387,7 +387,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return '' @app.route('/test_filters_without_returning_categories/') - def test_filters(): + def test_filters2(): messages = flask.get_flashed_messages(category_filter=['message', 'warning']) self.assert_equal(len(messages), 2) self.assert_equal(messages[0], u'Hello World') diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index c7cb0a8a..350eb7f2 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -145,6 +145,23 @@ class ViewTestCase(FlaskTestCase): self.assert_equal(rv.data, '') self.assert_equal(rv.headers['X-Method'], 'HEAD') + def test_endpoint_override(self): + app = flask.Flask(__name__) + app.debug = True + + class Index(flask.views.View): + methods = ['GET', 'POST'] + def dispatch_request(self): + return flask.request.method + + app.add_url_rule('/', view_func=Index.as_view('index')) + + with self.assert_raises(AssertionError): + app.add_url_rule('/', view_func=Index.as_view('index')) + + # But these tests should still pass. We just log a warning. + self.common_test(app) + def suite(): suite = unittest.TestSuite() From f701f699477800497636fd5b28132744ec68928d Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 17:13:12 +0200 Subject: [PATCH 1120/3747] Documented new error case --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 10a2fe1c..b79c6814 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,8 @@ Release date to be decided. ``template_filter`` method family. - Set the content-length header for x-sendfile. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. +- Flask will now raise an error if you attempt to register a new function + on an already used endpoint. Version 0.9 ----------- From 3afcbf160eff2a5ab6ac35a82e0719f972df8972 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 22:58:41 +0200 Subject: [PATCH 1121/3747] Extra safety for safe_join. Does not look exploitable but better safe than sorry. Fixes #501 --- flask/helpers.py | 4 +++- flask/testsuite/regression.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 9491ac55..9bcb22bb 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -604,7 +604,9 @@ def safe_join(directory, filename): for sep in _os_alt_seps: if sep in filename: raise NotFound() - if os.path.isabs(filename) or filename.startswith('../'): + if os.path.isabs(filename) or \ + filename == '..' or \ + filename.startswith('../'): raise NotFound() return os.path.join(directory, filename) diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index bc37afc4..87a6289b 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -17,6 +17,7 @@ import flask import threading import unittest from werkzeug.test import run_wsgi_app, create_environ +from werkzeug.exceptions import NotFound from flask.testsuite import FlaskTestCase @@ -79,6 +80,11 @@ class MemoryTestCase(FlaskTestCase): for x in xrange(10): fire() + def test_safe_join_toplevel_pardir(self): + from flask.helpers import safe_join + with self.assert_raises(NotFound): + safe_join('/foo', '..') + def suite(): suite = unittest.TestSuite() From 301e244df3dc747a73e2564a046976af5d0164eb Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 22:59:52 +0200 Subject: [PATCH 1122/3747] Consistent use of encoding naming --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 9bcb22bb..8780ee20 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -571,7 +571,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, os.path.getmtime(filename), os.path.getsize(filename), adler32( - filename.encode('utf8') if isinstance(filename, unicode) + filename.encode('utf-8') if isinstance(filename, unicode) else filename ) & 0xffffffff )) From b146d8277ab90cf6d43ea54113383076e4fd0318 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 23:31:48 +0200 Subject: [PATCH 1123/3747] Added wrapper module around simplejson/json for much simplified customization. --- CHANGES | 3 + docs/api.rst | 78 +++++++++++++++-------- flask/__init__.py | 14 +++-- flask/app.py | 16 ++++- flask/exceptions.py | 3 +- flask/helpers.py | 46 -------------- flask/json.py | 146 ++++++++++++++++++++++++++++++++++++++++++++ flask/sessions.py | 3 +- flask/wrappers.py | 2 +- 9 files changed, 227 insertions(+), 84 deletions(-) create mode 100644 flask/json.py diff --git a/CHANGES b/CHANGES index b79c6814..2a87cbe2 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,9 @@ Release date to be decided. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. - Flask will now raise an error if you attempt to register a new function on an already used endpoint. +- Added wrapper module around simplejson and added default serialization + of datetime objects. This allows much easier customization of how + JSON is handled by Flask or any Flask extension. Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 316c76ab..dbd1877e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -312,43 +312,71 @@ Message Flashing .. autofunction:: get_flashed_messages -Returning JSON --------------- +JSON Support +------------ + +.. module:: flask.json + +Flask uses ``simplejson`` for the JSON implementation. Since simplejson +is provided both by the standard library as well as extension Flask will +try simplejson first and then fall back to the stdlib json module. On top +of that it will delegate access to the current application's JSOn encoders +and decoders for easier customization. + +So for starters instead of doing:: + + try: + import simplejson as json + except ImportError: + import json + +You can instead just do this:: + + from flask import json + +For usage examples, read the :mod:`json` documentation in the standard +lirbary. The following extensions are by default applied to the stdlib's +JSON module: + +1. ``datetime`` objects are serialized as :rfc:`822` strings. +2. Any object with an ``__html__`` method (like :class:`~flask.Markup`) + will ahve that method called and then the return value is serialized + as string. + +The :func:`~htmlsafe_dumps` function of this json module is also available +as filter called ``|tojson`` in Jinja2. Note that inside `script` +tags no escaping must take place, so make sure to disable escaping +with ``|safe`` if you intend to use it inside `script` tags: + +.. sourcecode:: html+jinja + + <script type=text/javascript> + doSomethingWith({{ user.username|tojson|safe }}); + </script> + +Note that the ``|tojson`` filter escapes forward slashes properly. .. autofunction:: jsonify -.. data:: json +.. autofunction:: dumps - If JSON support is picked up, this will be the module that Flask is - using to parse and serialize JSON. So instead of doing this yourself:: +.. autofunction:: dump - try: - import simplejson as json - except ImportError: - import json +.. autofunction:: loads - You can instead just do this:: +.. autofunction:: load - from flask import json +.. autoclass:: JSONEncoder + :members: - For usage examples, read the :mod:`json` documentation. - - The :func:`~json.dumps` function of this json module is also available - as filter called ``|tojson`` in Jinja2. Note that inside `script` - tags no escaping must take place, so make sure to disable escaping - with ``|safe`` if you intend to use it inside `script` tags: - - .. sourcecode:: html+jinja - - <script type=text/javascript> - doSomethingWith({{ user.username|tojson|safe }}); - </script> - - Note that the ``|tojson`` filter escapes forward slashes properly. +.. autoclass:: JSONDecoder + :members: Template Rendering ------------------ +.. currentmodule:: flask + .. autofunction:: render_template .. autofunction:: render_template_string diff --git a/flask/__init__.py b/flask/__init__.py index fda94f39..6e7883fb 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -20,9 +20,8 @@ from jinja2 import Markup, escape from .app import Flask, Request, Response from .config import Config -from .helpers import url_for, jsonify, flash, \ - send_file, send_from_directory, get_flashed_messages, \ - get_template_attribute, make_response, safe_join, \ +from .helpers import url_for, flash, send_file, send_from_directory, \ + get_flashed_messages, get_template_attribute, make_response, safe_join, \ stream_with_context from .globals import current_app, g, request, session, _request_ctx_stack, \ _app_ctx_stack @@ -37,8 +36,13 @@ from .signals import signals_available, template_rendered, request_started, \ request_finished, got_request_exception, request_tearing_down, \ appcontext_tearing_down -# only import json if it's available -from .helpers import json +# We're not exposing the actual json module but a convenient wrapper around +# it. +from . import json + +# This was the only thing that flask used to export at one point and it had +# a more generic name. +jsonify = json.jsonify # backwards compat, goes away in 1.0 from .sessions import SecureCookieSession as Session diff --git a/flask/app.py b/flask/app.py index 327cb8cf..1763aa74 100644 --- a/flask/app.py +++ b/flask/app.py @@ -24,8 +24,8 @@ from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed, BadRequest from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ - find_package + locked_cached_property, _endpoint_from_view_func, find_package +from . import json from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import RequestContext, AppContext, _RequestGlobals @@ -238,6 +238,16 @@ class Flask(_PackageBoundObject): '-' * 80 ) + #: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`. + #: + #: .. versionadded:: 0.10 + json_encoder = json.JSONEncoder + + #: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`. + #: + #: .. versionadded:: 0.10 + json_decoder = json.JSONDecoder + #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] @@ -637,7 +647,7 @@ class Flask(_PackageBoundObject): url_for=url_for, get_flashed_messages=get_flashed_messages ) - rv.filters['tojson'] = _tojson_filter + rv.filters['tojson'] = json.htmlsafe_dumps return rv def create_global_jinja_loader(self): diff --git a/flask/exceptions.py b/flask/exceptions.py index 9ccdedab..83b9556b 100644 --- a/flask/exceptions.py +++ b/flask/exceptions.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for more details. """ from werkzeug.exceptions import HTTPException, BadRequest -from .helpers import json +from . import json class JSONHTTPException(HTTPException): @@ -39,7 +39,6 @@ class JSONHTTPException(HTTPException): class JSONBadRequest(JSONHTTPException, BadRequest): """Represents an HTTP ``400 Bad Request`` error whose body contains an error message in JSON format instead of HTML format (as in the superclass). - """ #: The description of the error which occurred as a string. diff --git a/flask/helpers.py b/flask/helpers.py index 8780ee20..3f06c116 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -23,10 +23,6 @@ from werkzeug.routing import BuildError from werkzeug.urls import url_quote from functools import update_wrapper -# Use the same json implementation as itsdangerous on which we -# depend anyways. -from itsdangerous import simplejson as json - from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound @@ -43,17 +39,6 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request -# figure out if simplejson escapes slashes. This behavior was changed -# from one version to another without reason. -_slash_escape = '\\/' not in json.dumps('/') - -def _tojson_filter(*args, **kwargs): - rv = json.dumps(*args, **kwargs) - if _slash_escape: - rv = rv.replace('/', '\\/') - return rv.replace('<!', '<\\u0021') - - # sentinel _missing = object() @@ -146,37 +131,6 @@ def stream_with_context(generator_or_function): return wrapped_g -def jsonify(*args, **kwargs): - """Creates a :class:`~flask.Response` with the JSON representation of - the given arguments with an `application/json` mimetype. The arguments - to this function are the same as to the :class:`dict` constructor. - - Example usage:: - - @app.route('/_get_current_user') - def get_current_user(): - return jsonify(username=g.user.username, - email=g.user.email, - id=g.user.id) - - This will send a JSON response like this to the browser:: - - { - "username": "admin", - "email": "admin@localhost", - "id": 42 - } - - This requires Python 2.6 or an installed version of simplejson. For - security reasons only objects are supported toplevel. For more - information about this, have a look at :ref:`json-security`. - - .. versionadded:: 0.2 - """ - return current_app.response_class(json.dumps(dict(*args, **kwargs), - indent=None if request.is_xhr else 2), mimetype='application/json') - - def make_response(*args): """Sometimes it is necessary to set additional headers in a view. Because views do not have to return response objects but can return a value that diff --git a/flask/json.py b/flask/json.py new file mode 100644 index 00000000..149cfbab --- /dev/null +++ b/flask/json.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +""" + flask.jsonimpl + ~~~~~~~~~~~~~~ + + Implementation helpers for the JSON support in Flask. + + :copyright: (c) 2012 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from datetime import datetime +from .globals import current_app, request + +from werkzeug.http import http_date + +# Use the same json implementation as itsdangerous on which we +# depend anyways. +from itsdangerous import simplejson as _json + + +# figure out if simplejson escapes slashes. This behavior was changed +# from one version to another without reason. +_slash_escape = '\\/' not in _json.dumps('/') + + +__all__ = ['dump', 'dumps', 'load', 'loads', 'htmlsafe_dump', + 'htmlsafe_dumps', 'JSONDecoder', 'JSONEncoder', + 'jsonify'] + + +class JSONEncoder(_json.JSONEncoder): + """The default Flask JSON encoder. This one extends the default simplejson + encoder by also supporting ``datetime`` objects as well as ``Markup`` + objects which are serialized as RFC 822 datetime strings (same as the HTTP + date format). In order to support more data types override the + :meth:`default` method. + """ + + def default(self, o): + """Implement this method in a subclass such that it returns a + serializable object for ``o``, or calls the base implementation (to + raise a ``TypeError``). + + For example, to support arbitrary iterators, you could implement + default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + """ + if isinstance(o, datetime): + return http_date(o) + if hasattr(o, '__html__'): + return unicode(o.__html__()) + return _json.JSONEncoder.default(self, o) + + +class JSONDecoder(_json.JSONDecoder): + """The default JSON decoder. This one does not change the behavior from + the default simplejson encoder. Consult the :mod:`json` documentation + for more information. This decoder is not only used for the load + functions of this module but also :attr:`~flask.Request`. + """ + + +def dumps(obj, **kwargs): + """Serialize ``obj`` to a JSON formatted ``str`` by using the application's + configured encoder (:attr:`~flask.Flask.json_encoder`). + """ + kwargs.setdefault('cls', current_app.json_encoder) + return _json.dumps(obj, **kwargs) + + +def dump(obj, fp, **kwargs): + """Like :func:`dumps` but writes into a file object.""" + kwargs.setdefault('cls', current_app.json_encoder) + return _json.dump(obj, fp, **kwargs) + + +def loads(s, **kwargs): + """Unserialize a JSON object from a string ``s`` by using the application's + configured decoder (:attr:`~flask.Flask.json_decoder`). + """ + kwargs.setdefault('cls', current_app.json_decoder) + return _json.loads(s, **kwargs) + + +def load(fp, **kwargs): + """Like :func:`loads` but reads from a file object. + """ + kwargs.setdefault('cls', current_app.json_decoder) + return _json.load(fp, **kwargs) + + +def htmlsafe_dumps(obj, **kwargs): + """Works exactly like :func:`dumps` but is safe for use in ``<script>`` + tags. It accepts the same arguments and returns a JSON string. Note that + this is available in templates through the ``|tojson`` filter but it will + have to be wrapped in ``|safe`` unless **true** XHTML is being used. + """ + rv = dumps(obj, **kwargs) + if _slash_escape: + rv = rv.replace('/', '\\/') + return rv.replace('<!', '<\\u0021') + + +def htmlsafe_dump(obj, fp, **kwargs): + """Like :func:`htmlsafe_dumps` but writes into a file object.""" + fp.write(htmlsafe_dumps(obj, **kwargs)) + + +def jsonify(*args, **kwargs): + """Creates a :class:`~flask.Response` with the JSON representation of + the given arguments with an `application/json` mimetype. The arguments + to this function are the same as to the :class:`dict` constructor. + + Example usage:: + + @app.route('/_get_current_user') + def get_current_user(): + return jsonify(username=g.user.username, + email=g.user.email, + id=g.user.id) + + This will send a JSON response like this to the browser:: + + { + "username": "admin", + "email": "admin@localhost", + "id": 42 + } + + This requires Python 2.6 or an installed version of simplejson. For + security reasons only objects are supported toplevel. For more + information about this, have a look at :ref:`json-security`. + + .. versionadded:: 0.2 + """ + return current_app.response_class(dumps(dict(*args, **kwargs), + indent=None if request.is_xhr else 2), + mimetype='application/json') diff --git a/flask/sessions.py b/flask/sessions.py index 343fa94d..46ce0830 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -13,8 +13,7 @@ import hashlib from datetime import datetime from werkzeug.http import http_date, parse_date from werkzeug.datastructures import CallbackDict -from .helpers import json -from . import Markup +from . import Markup, json from itsdangerous import URLSafeTimedSerializer, BadSignature diff --git a/flask/wrappers.py b/flask/wrappers.py index cd1a6ec9..a56fe5d7 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -14,7 +14,7 @@ from werkzeug.utils import cached_property from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict -from .helpers import json +from . import json from .globals import _request_ctx_stack From 05c6502cbde016f0475233faa0c6364634cccac9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 7 Oct 2012 23:41:41 +0200 Subject: [PATCH 1124/3747] Let json.* work even without app on the stack and added tests --- flask/json.py | 18 ++++++++++++------ flask/testsuite/helpers.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/flask/json.py b/flask/json.py index 149cfbab..1593a326 100644 --- a/flask/json.py +++ b/flask/json.py @@ -70,30 +70,36 @@ class JSONDecoder(_json.JSONDecoder): def dumps(obj, **kwargs): """Serialize ``obj`` to a JSON formatted ``str`` by using the application's - configured encoder (:attr:`~flask.Flask.json_encoder`). + configured encoder (:attr:`~flask.Flask.json_encoder`) if there is an + application on the stack. """ - kwargs.setdefault('cls', current_app.json_encoder) + if current_app: + kwargs.setdefault('cls', current_app.json_encoder) return _json.dumps(obj, **kwargs) def dump(obj, fp, **kwargs): """Like :func:`dumps` but writes into a file object.""" - kwargs.setdefault('cls', current_app.json_encoder) + if current_app: + kwargs.setdefault('cls', current_app.json_encoder) return _json.dump(obj, fp, **kwargs) def loads(s, **kwargs): """Unserialize a JSON object from a string ``s`` by using the application's - configured decoder (:attr:`~flask.Flask.json_decoder`). + configured decoder (:attr:`~flask.Flask.json_decoder`) if there is an + application on the stack. """ - kwargs.setdefault('cls', current_app.json_decoder) + if current_app: + kwargs.setdefault('cls', current_app.json_decoder) return _json.loads(s, **kwargs) def load(fp, **kwargs): """Like :func:`loads` but reads from a file object. """ - kwargs.setdefault('cls', current_app.json_decoder) + if current_app: + kwargs.setdefault('cls', current_app.json_decoder) return _json.load(fp, **kwargs) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 58592668..ee5312e0 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -100,6 +100,36 @@ class JSONTestCase(FlaskTestCase): rv = render('{{ "<!--<script>"|tojson|safe }}') self.assert_equal(rv, '"<\\u0021--<script>"') + def test_json_customization(self): + class X(object): + def __init__(self, val): + self.val = val + class MyEncoder(flask.json.JSONEncoder): + def default(self, o): + if isinstance(o, X): + return '<%d>' % o.val + return flask.json.JSONEncoder.default(self, o) + class MyDecoder(flask.json.JSONDecoder): + def __init__(self, *args, **kwargs): + kwargs.setdefault('object_hook', self.object_hook) + flask.json.JSONDecoder.__init__(self, *args, **kwargs) + def object_hook(self, obj): + if len(obj) == 1 and '_foo' in obj: + return X(obj['_foo']) + return obj + app = flask.Flask(__name__) + app.testing = True + app.json_encoder = MyEncoder + app.json_decoder = MyDecoder + @app.route('/', methods=['POST']) + def index(): + return flask.json.dumps(flask.request.json['x']) + c = app.test_client() + rv = c.post('/', data=flask.json.dumps({ + 'x': {'_foo': 42} + }), content_type='application/json') + self.assertEqual(rv.data, '"<42>"') + def test_modified_url_encoding(self): class ModifiedRequest(flask.Request): url_charset = 'euc-kr' From 5e88c8184d19851c4ccc9a74595819bbe374e839 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 8 Oct 2012 06:48:13 +0200 Subject: [PATCH 1125/3747] Removed deprecated and awkward flask.session module --- CHANGES | 3 +++ flask/session.py | 19 ------------------- 2 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 flask/session.py diff --git a/CHANGES b/CHANGES index 2a87cbe2..00978cdd 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,9 @@ Release date to be decided. - Added wrapper module around simplejson and added default serialization of datetime objects. This allows much easier customization of how JSON is handled by Flask or any Flask extension. +- Removed deprecated internal ``flask.session`` module alias. Use + ``flask.sessions`` instead to get the session module. This is not to + be confused with ``flask.session`` the session proxy. Version 0.9 ----------- diff --git a/flask/session.py b/flask/session.py deleted file mode 100644 index 1a43fdc1..00000000 --- a/flask/session.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.session - ~~~~~~~~~~~~~ - - This module used to flask with the session global so we moved it - over to flask.sessions - - :copyright: (c) 2011 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" - -from warnings import warn -warn(DeprecationWarning('please use flask.sessions instead')) - -from .sessions import SecureCookieSession, NullSession - -Session = SecureCookieSession -_NullSession = NullSession From f34c0281252bf1838e2ec24fe8b064b232a098ef Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 8 Oct 2012 07:01:49 +0200 Subject: [PATCH 1126/3747] Added template tests and made config a true global --- CHANGES | 7 +++++++ docs/templating.rst | 15 ++++++++++++--- flask/app.py | 11 +++++++---- flask/templating.py | 9 +++++---- flask/testsuite/templating.py | 12 ++++++++++++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 00978cdd..fd9d110c 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,13 @@ Release date to be decided. - Removed deprecated internal ``flask.session`` module alias. Use ``flask.sessions`` instead to get the session module. This is not to be confused with ``flask.session`` the session proxy. +- Templates can now be rendered without request context. The behavior is + slightly different as the ``request``, ``session`` and ``g`` objects + will not be available and blueprint's context processors are not + called. +- The config object is now available to the template as a real global and + not through a context processor which makes it available even in imported + templates by default. Version 0.9 ----------- diff --git a/docs/templating.rst b/docs/templating.rst index 8ecf5332..166f26aa 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -38,20 +38,29 @@ by default: .. versionadded:: 0.6 + .. versionchanged:: 0.10 + This is now always available, even in imported templates. + .. data:: request :noindex: - The current request object (:class:`flask.request`) + The current request object (:class:`flask.request`). This variable is + unavailable if the template was rendered without an active request + context. .. data:: session :noindex: - The current session object (:class:`flask.session`) + The current session object (:class:`flask.session`). This variable + is unavailable if the template was rendered without an active request + context. .. data:: g :noindex: - The request-bound object for global variables (:data:`flask.g`) + The request-bound object for global variables (:data:`flask.g`). This + variable is unavailable if the template was rendered without an active + request context. .. function:: url_for :noindex: diff --git a/flask/app.py b/flask/app.py index 1763aa74..87ba933e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -645,7 +645,8 @@ class Flask(_PackageBoundObject): rv = Environment(self, **options) rv.globals.update( url_for=url_for, - get_flashed_messages=get_flashed_messages + get_flashed_messages=get_flashed_messages, + config=self.config ) rv.filters['tojson'] = json.htmlsafe_dumps return rv @@ -694,9 +695,11 @@ class Flask(_PackageBoundObject): to add extra variables. """ funcs = self.template_context_processors[None] - bp = _request_ctx_stack.top.request.blueprint - if bp is not None and bp in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[bp]) + reqctx = _request_ctx_stack.top + if reqctx is not None: + bp = reqctx.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() for func in funcs: context.update(func()) diff --git a/flask/templating.py b/flask/templating.py index e5d896cb..2bac22e9 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -12,7 +12,7 @@ import posixpath from jinja2 import BaseLoader, Environment as BaseEnvironment, \ TemplateNotFound -from .globals import _request_ctx_stack +from .globals import _request_ctx_stack, _app_ctx_stack from .signals import template_rendered from .module import blueprint_is_module @@ -22,8 +22,9 @@ def _default_template_ctx_processor(): `session` and `g`. """ reqctx = _request_ctx_stack.top + if reqctx is None: + return {} return dict( - config=reqctx.app.config, request=reqctx.request, session=reqctx.session, g=reqctx.g @@ -119,7 +120,7 @@ def render_template(template_name_or_list, **context): :param context: the variables that should be available in the context of the template. """ - ctx = _request_ctx_stack.top + ctx = _app_ctx_stack.top ctx.app.update_template_context(context) return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list), context, ctx.app) @@ -134,7 +135,7 @@ def render_template_string(source, **context): :param context: the variables that should be available in the context of the template. """ - ctx = _request_ctx_stack.top + ctx = _app_ctx_stack.top ctx.app.update_template_context(context) return _render(ctx.app.jinja_env.from_string(source), context, ctx.app) diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 1df4292d..6345b710 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -37,6 +37,18 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, '42') + def test_request_less_rendering(self): + app = flask.Flask(__name__) + app.config['WORLD_NAME'] = 'Special World' + @app.context_processor + def context_processor(): + return dict(foo=42) + + with app.app_context(): + rv = flask.render_template_string('Hello {{ config.WORLD_NAME }} ' + '{{ foo }}') + self.assert_equal(rv, 'Hello Special World 42') + def test_standard_context(self): app = flask.Flask(__name__) app.secret_key = 'development key' From 3e9f4e254b5757d80218c0d8077bd44465365489 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 8 Oct 2012 07:05:32 +0200 Subject: [PATCH 1127/3747] Updated a comment that was misleading with recent flask sqlalchemy installations --- flask/ctx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask/ctx.py b/flask/ctx.py index 3ea42a27..84e96575 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -213,7 +213,7 @@ class RequestContext(object): # on the stack. The rationale is that you want to access that # information under debug situations. However if someone forgets to # pop that context again we want to make sure that on the next push - # it's invalidated otherwise we run at risk that something leaks + # it's invalidated, otherwise we run at risk that something leaks # memory. This is usually only a problem in testsuite since this # functionality is not active in production environments. top = _request_ctx_stack.top @@ -234,7 +234,8 @@ class RequestContext(object): # Open the session at the moment that the request context is # available. This allows a custom open_session method to use the - # request context (e.g. flask-sqlalchemy). + # request context (e.g. code that access database information + # stored on `g` instead of the appcontext). self.session = self.app.open_session(self.request) if self.session is None: self.session = self.app.make_null_session() From c2e5799879ed22a4657b75df6da66d6074ae7cc7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 9 Oct 2012 14:02:32 -0500 Subject: [PATCH 1128/3747] Updated examples to new sqlite patterns and added new section to appcontext docs --- docs/appcontext.rst | 44 ++++++++++++++ docs/patterns/sqlite3.rst | 107 +++++++++++++++++++--------------- examples/flaskr/flaskr.py | 41 +++++++------ examples/minitwit/minitwit.py | 81 +++++++++++++------------ 4 files changed, 169 insertions(+), 104 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 928ffc8a..a6b67001 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -85,3 +85,47 @@ Extensions are free to store additional information on the topmost level, assuming they pick a sufficiently unique name. For more information about that, see :ref:`extension-dev`. + +Context Usage +------------- + +The context is typically used to cache resources on there that need to be +created on a per-request or usage case. For instance database connects +are destined to go there. When storing things on the application context +unique names should be chosen as this is a place that is shared between +Flask applications and extensions. + +The most common usage is to split resource management into two parts: + +1. an implicit resource caching on the context. +2. a context teardown based resource deallocation. + +Generally there would be a ``get_X()`` function that creates resource +``X`` if it does not exist yet and otherwise returns the same resource, +and a ``teardown_X()`` function that is registered as teardown handler. + +This is an example that connects to a database:: + + import sqlite3 + from flask import _app_ctx_stack + + def get_db(): + top = _app_ctx_stack.top + if not hasattr(top, 'database'): + top.database = connect_to_database() + return top.database + + @app.teardown_appcontext + def teardown_db(exception): + top = _app_ctx_stack.top + if hasattr(top, 'database'): + top.database.close() + +The first time ``get_db()`` is called the connection will be established. +To make this implicit a :class:`~werkzeug.local.LocalProxy` can be used:: + + from werkzeug.local import LocalProxy + db = LocalProxy(get_db) + +That way a user can directly access ``db`` which internally calls +``get_db()``. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 0d02e465..45bcc959 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,61 +3,61 @@ Using SQLite 3 with Flask ========================= -In Flask you can implement the opening of database connections at the -beginning of the request and closing at the end with the -:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.teardown_request` -decorators in combination with the special :class:`~flask.g` object. +In Flask you can implement the opening of database connections on demand +and closing it when the context dies (usually at the end of the request) +easily. -So here is a simple example of how you can use SQLite 3 with Flask:: +Here is a simple example of how you can use SQLite 3 with Flask:: import sqlite3 - from flask import g + from flask import _app_ctx_stack DATABASE = '/path/to/database.db' - def connect_db(): - return sqlite3.connect(DATABASE) + def get_db(): + top = _app_ctx_stack.top + if not hasattr(top, 'sqlite_db'): + top.sqlite_db = sqlite3.connect(DATABASE) + return top.sqlite_db - @app.before_request - def before_request(): - g.db = connect_db() + @app.teardown_appcontext + def close_connection(exception): + top = _app_ctx_stack.top + if hasattr(top, 'sqlite_db'): + top.sqlite_db.close() + +All the application needs to do in order to now use the database is having +an active application context (which is always true if there is an request +in flight) or to create an application context itself. At that point the +``get_db`` function can be used to get the current database connection. +Whenever the context is destroyed the database connection will be +terminated. + +Example:: + + @app.route('/') + def index(): + cur = get_db().cursor() + ... - @app.teardown_request - def teardown_request(exception): - if hasattr(g, 'db'): - g.db.close() .. note:: - Please keep in mind that the teardown request functions are always - executed, even if a before-request handler failed or was never - executed. Because of this we have to make sure here that the database - is there before we close it. + Please keep in mind that the teardown request and appcontext functions + are always executed, even if a before-request handler failed or was + never executed. Because of this we have to make sure here that the + database is there before we close it. Connect on Demand ----------------- -The downside of this approach is that this will only work if Flask -executed the before-request handlers for you. If you are attempting to -use the database from a script or the interactive Python shell you would -have to do something like this:: +The upside of this approach (connecting on first use) is that this will +only opening the connection if truly necessary. If you want to use this +code outside a request context you can use it in a Python shell by opening +the application context by hand:: - with app.test_request_context(): - app.preprocess_request() - # now you can use the g.db object - -In order to trigger the execution of the connection code. You won't be -able to drop the dependency on the request context this way, but you could -make it so that the application connects when necessary:: - - def get_connection(): - db = getattr(g, '_db', None) - if db is None: - db = g._db = connect_db() - return db - -Downside here is that you have to use ``db = get_connection()`` instead of -just being able to use ``g.db`` directly. + with app.app_context(): + # now you can use get_db() .. _easy-querying: @@ -66,16 +66,28 @@ Easy Querying Now in each request handling function you can access `g.db` to get the current open database connection. To simplify working with SQLite, a -helper function can be useful:: +row factory function is useful. It is executed for every result returned +from the database to convert the result. For instance in order to get +dictionaries instead of tuples this can be used:: + def make_dicts(cursor, row): + return dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) + + db.row_factory = make_dicts + +Additionally it is a good idea to provide a query function that combines +getting the cursor, executing and fetching the results:: + def query_db(query, args=(), one=False): - cur = g.db.execute(query, args) - rv = [dict((cur.description[idx][0], value) - for idx, value in enumerate(row)) for row in cur.fetchall()] + cur = get_db().execute(query, args) + rv = cur.fetchall() + cur.close() return (rv[0] if rv else None) if one else rv -This handy little function makes working with the database much more -pleasant than it is by just using the raw cursor and connection objects. +This handy little function in combination with a row factory makes working +with the database much more pleasant than it is by just using the raw +cursor and connection objects. Here is how you can use it:: @@ -105,10 +117,9 @@ Relational databases need schemas, so applications often ship a a function that creates the database based on that schema. This function can do that for you:: - from contextlib import closing - def init_db(): - with closing(connect_db()) as db: + with app.app_context(): + db = get_db() with app.open_resource('schema.sql') as f: db.cursor().executescript(f.read()) db.commit() diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 6f9b06fc..8526254d 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -11,9 +11,8 @@ """ from __future__ import with_statement from sqlite3 import dbapi2 as sqlite3 -from contextlib import closing from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash + render_template, flash, _app_ctx_stack # configuration DATABASE = '/tmp/flaskr.db' @@ -28,35 +27,37 @@ app.config.from_object(__name__) app.config.from_envvar('FLASKR_SETTINGS', silent=True) -def connect_db(): - """Returns a new connection to the database.""" - return sqlite3.connect(app.config['DATABASE']) - - def init_db(): """Creates the database tables.""" - with closing(connect_db()) as db: + with app.app_context(): + db = get_db() with app.open_resource('schema.sql') as f: db.cursor().executescript(f.read()) db.commit() -@app.before_request -def before_request(): - """Make sure we are connected to the database each request.""" - g.db = connect_db() +def get_db(): + """Opens a new database connection if there is none yet for the + current application context. + """ + top = _app_ctx_stack.top + if not hasattr(top, 'sqlite_db'): + top.sqlite_db = sqlite3.connect(app.config['DATABASE']) + return top.sqlite_db -@app.teardown_request -def teardown_request(exception): +@app.teardown_appcontext +def close_db_connection(exception): """Closes the database again at the end of the request.""" - if hasattr(g, 'db'): - g.db.close() + top = _app_ctx_stack.top + if hasattr(top, 'sqlite_db'): + top.sqlite_db.close() @app.route('/') def show_entries(): - cur = g.db.execute('select title, text from entries order by id desc') + db = get_db() + cur = db.execute('select title, text from entries order by id desc') entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] return render_template('show_entries.html', entries=entries) @@ -65,9 +66,10 @@ def show_entries(): def add_entry(): if not session.get('logged_in'): abort(401) - g.db.execute('insert into entries (title, text) values (?, ?)', + db = get_db() + db.execute('insert into entries (title, text) values (?, ?)', [request.form['title'], request.form['text']]) - g.db.commit() + db.commit() flash('New entry was successfully posted') return redirect(url_for('show_entries')) @@ -95,4 +97,5 @@ def logout(): if __name__ == '__main__': + init_db() app.run() diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index fee023fd..a92da6af 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -13,9 +13,8 @@ import time from sqlite3 import dbapi2 as sqlite3 from hashlib import md5 from datetime import datetime -from contextlib import closing from flask import Flask, request, session, url_for, redirect, \ - render_template, abort, g, flash + render_template, abort, g, flash, _app_ctx_stack from werkzeug import check_password_hash, generate_password_hash @@ -31,14 +30,29 @@ app.config.from_object(__name__) app.config.from_envvar('MINITWIT_SETTINGS', silent=True) -def connect_db(): - """Returns a new connection to the database.""" - return sqlite3.connect(app.config['DATABASE']) +def get_db(): + """Opens a new database connection if there is none yet for the + current application context. + """ + top = _app_ctx_stack.top + if not hasattr(top, 'sqlite_db'): + top.sqlite_db = sqlite3.connect(app.config['DATABASE']) + top.sqlite_db.row_factory = sqlite3.Row + return top.sqlite_db + + +@app.teardown_appcontext +def close_database(exception): + """Closes the database again at the end of the request.""" + top = _app_ctx_stack.top + if hasattr(top, 'sqlite_db'): + top.sqlite_db.close() def init_db(): """Creates the database tables.""" - with closing(connect_db()) as db: + with app.app_context(): + db = get_db() with app.open_resource('schema.sql') as f: db.cursor().executescript(f.read()) db.commit() @@ -46,16 +60,15 @@ def init_db(): def query_db(query, args=(), one=False): """Queries the database and returns a list of dictionaries.""" - cur = g.db.execute(query, args) - rv = [dict((cur.description[idx][0], value) - for idx, value in enumerate(row)) for row in cur.fetchall()] + cur = get_db().execute(query, args) + rv = cur.fetchall() return (rv[0] if rv else None) if one else rv def get_user_id(username): """Convenience method to look up the id for a username.""" - rv = g.db.execute('select user_id from user where username = ?', - [username]).fetchone() + rv = query_db('select user_id from user where username = ?', + [username], one=True) return rv[0] if rv else None @@ -72,23 +85,12 @@ def gravatar_url(email, size=80): @app.before_request def before_request(): - """Make sure we are connected to the database each request and look - up the current user so that we know he's there. - """ - g.db = connect_db() g.user = None if 'user_id' in session: g.user = query_db('select * from user where user_id = ?', [session['user_id']], one=True) -@app.teardown_request -def teardown_request(exception): - """Closes the database again at the end of the request.""" - if hasattr(g, 'db'): - g.db.close() - - @app.route('/') def timeline(): """Shows a users timeline or if no user is logged in it will @@ -145,9 +147,10 @@ def follow_user(username): whom_id = get_user_id(username) if whom_id is None: abort(404) - g.db.execute('insert into follower (who_id, whom_id) values (?, ?)', - [session['user_id'], whom_id]) - g.db.commit() + db = get_db() + db.execute('insert into follower (who_id, whom_id) values (?, ?)', + [session['user_id'], whom_id]) + db.commit() flash('You are now following "%s"' % username) return redirect(url_for('user_timeline', username=username)) @@ -160,9 +163,10 @@ def unfollow_user(username): whom_id = get_user_id(username) if whom_id is None: abort(404) - g.db.execute('delete from follower where who_id=? and whom_id=?', - [session['user_id'], whom_id]) - g.db.commit() + db = get_db() + db.execute('delete from follower where who_id=? and whom_id=?', + [session['user_id'], whom_id]) + db.commit() flash('You are no longer following "%s"' % username) return redirect(url_for('user_timeline', username=username)) @@ -173,10 +177,11 @@ def add_message(): if 'user_id' not in session: abort(401) if request.form['text']: - g.db.execute('''insert into message (author_id, text, pub_date) - values (?, ?, ?)''', (session['user_id'], request.form['text'], - int(time.time()))) - g.db.commit() + db = get_db() + db.execute('''insert into message (author_id, text, pub_date) + values (?, ?, ?)''', (session['user_id'], request.form['text'], + int(time.time()))) + db.commit() flash('Your message was recorded') return redirect(url_for('timeline')) @@ -221,11 +226,12 @@ def register(): elif get_user_id(request.form['username']) is not None: error = 'The username is already taken' else: - g.db.execute('''insert into user ( - username, email, pw_hash) values (?, ?, ?)''', - [request.form['username'], request.form['email'], - generate_password_hash(request.form['password'])]) - g.db.commit() + db = get_db() + db.execute('''insert into user ( + username, email, pw_hash) values (?, ?, ?)''', + [request.form['username'], request.form['email'], + generate_password_hash(request.form['password'])]) + db.commit() flash('You were successfully registered and can login now') return redirect(url_for('login')) return render_template('register.html', error=error) @@ -245,4 +251,5 @@ app.jinja_env.filters['gravatar'] = gravatar_url if __name__ == '__main__': + init_db() app.run() From f6a5a7a0cc91ccd06e89ac0820dd6d7bdf2c2ef1 Mon Sep 17 00:00:00 2001 From: Corbin Simpson <MostAwesomeDude@gmail.com> Date: Thu, 11 Oct 2012 14:05:01 -0700 Subject: [PATCH 1129/3747] docs/deploying/wsgi-standalone: Add Twisted Web. I've been meaning to do this for quite some time, but I never got around to it. Hopefully this is neutral and useful enough to be included in the main docs. --- docs/deploying/wsgi-standalone.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index 74385813..7a3ef0a9 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -66,6 +66,29 @@ event loop:: .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ +Twisted Web +----------- + +`Twisted Web`_ is the web server shipped with `Twisted`_, a mature, +non-blocking event-driven networking library. Twisted Web comes with a +standard WSGI container which can be controlled from the command line using +the ``twistd`` utility:: + + twistd web --wsgi myproject.app + +This example will run a Flask application called ``app`` from a module named +``myproject``. + +Twisted Web supports many flags and options, and the ``twistd`` utility does +as well; see ``twistd -h`` and ``twistd web -h`` for more information. For +example, to run a Twisted Web server in the foreground, on port 8080, with an +application from ``myproject``:: + + twistd -n web --port 8080 --wsgi myproject.app + +.. _Twisted: https://twistedmatrix.com/ +.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb + .. _deploying-proxy-setups: Proxy Setups From 7ee40e9348fba36d57727d7aa58d802b191c0197 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer <markus@unterwaditzer.net> Date: Tue, 16 Oct 2012 21:56:51 +0200 Subject: [PATCH 1130/3747] Fix #611 Minor but confusing typo in tutorial. --- docs/tutorial/dbinit.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 79479397..6415cdaa 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -32,7 +32,7 @@ add the following lines to your existing imports in `flaskr.py`:: Next we can create a function called `init_db` that initializes the database. For this we can use the `connect_db` function we defined earlier. Just add that function below the `connect_db` function in -`flask.py`:: +`flaskr.py`:: def init_db(): with closing(connect_db()) as db: From a15c6c569ab40f0da2d18b1bd338bd03ddb024eb Mon Sep 17 00:00:00 2001 From: Mitchell Peabody <mitchell.peabody@qrclab.com> Date: Tue, 16 Oct 2012 16:57:57 -0400 Subject: [PATCH 1131/3747] The builder on github is using python 2.5, the views.py testsuite uses the with statement, and thus flask/testsuite/views.py requires from __future__ import with_statement at the beginning. --- flask/testsuite/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index 350eb7f2..f09c1266 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -8,13 +8,13 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement import flask import flask.views import unittest from flask.testsuite import FlaskTestCase from werkzeug.http import parse_set_header - class ViewTestCase(FlaskTestCase): def common_test(self, app): From 275f830c8346c29af424b1ed2f2ffabee2408ec8 Mon Sep 17 00:00:00 2001 From: Mitchell Peabody <mitchell.peabody@qrclab.com> Date: Wed, 17 Oct 2012 11:56:39 -0400 Subject: [PATCH 1132/3747] There was a duplicated call to url_adapter.build(...) try: rv = url_adapter.build(endpoint, values, method=method, force_external=external) except BuildError, error: # We need to inject the values again so that the app callback can # deal with that sort of stuff. values['_external'] = external values['_anchor'] = anchor values['_method'] = method return appctx.app.handle_url_build_error(error, endpoint, values) rv = url_adapter.build(endpoint, values, method=method, force_external=external) If no exception was raised for url_adapter.build(...) then the same method call would be made after the try...except block. This is unnecessary. --- flask/helpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 3f06c116..6aea45c6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -295,8 +295,6 @@ def url_for(endpoint, **values): values['_method'] = method return appctx.app.handle_url_build_error(error, endpoint, values) - rv = url_adapter.build(endpoint, values, method=method, - force_external=external) if anchor is not None: rv += '#' + url_quote(anchor) return rv From 8339cb35082fc93d3a9c98616aed7fe0c1c642dc Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Thu, 18 Oct 2012 00:48:15 +0100 Subject: [PATCH 1133/3747] Added support for unicode json dumping. This fixes #535 --- CHANGES | 4 ++++ docs/config.rst | 10 ++++++++++ flask/app.py | 3 ++- flask/json.py | 31 +++++++++++++++++++++++-------- flask/testsuite/helpers.py | 13 +++++++++++++ 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index fd9d110c..6a1ee7c7 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,10 @@ Release date to be decided. - The config object is now available to the template as a real global and not through a context processor which makes it available even in imported templates by default. +- Added an option to generate non-ascii encoded JSON which should result + in less bytes being transmitted over the network. It's disabled by + default to not cause confusion with existing libraries that might expect + ``flask.json.dumps`` to return bytestrings by default. Version 0.9 ----------- diff --git a/docs/config.rst b/docs/config.rst index 7a32fb84..134fe36d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -142,6 +142,13 @@ The following configuration values are used internally by Flask: ``PREFERRED_URL_SCHEME`` The URL scheme that should be used for URL generation if no URL scheme is available. This defaults to ``http``. +``JSON_AS_ASCII`` By default Flask serialize object to + ascii-encoded JSON. If this is set to + ``False`` Flask will not encode to ASCII + and output strings as-is and return + unicode strings. ``jsonfiy`` will + automatically encode it in ``utf-8`` + then for transport for instance. ================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -184,6 +191,9 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.9 ``PREFERRED_URL_SCHEME`` +.. versionadded:: 0.10 + ``JSON_AS_ASCII`` + Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 87ba933e..9d291b3a 100644 --- a/flask/app.py +++ b/flask/app.py @@ -274,7 +274,8 @@ class Flask(_PackageBoundObject): 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False, - 'PREFERRED_URL_SCHEME': 'http' + 'PREFERRED_URL_SCHEME': 'http', + 'JSON_AS_ASCII': True }) #: The rule object to use for URL rules created. This is used by diff --git a/flask/json.py b/flask/json.py index 1593a326..a85bdc63 100644 --- a/flask/json.py +++ b/flask/json.py @@ -68,20 +68,37 @@ class JSONDecoder(_json.JSONDecoder): """ +def _dump_arg_defaults(kwargs): + """Inject default arguments for dump functions.""" + if current_app: + kwargs.setdefault('cls', current_app.json_encoder) + if not current_app.config['JSON_AS_ASCII']: + kwargs.setdefault('ensure_ascii', False) + + +def _load_arg_defaults(kwargs): + """Inject default arguments for load functions.""" + if current_app: + kwargs.setdefault('cls', current_app.json_decoder) + + def dumps(obj, **kwargs): """Serialize ``obj`` to a JSON formatted ``str`` by using the application's configured encoder (:attr:`~flask.Flask.json_encoder`) if there is an application on the stack. + + This function can return ``unicode`` strings or ascii-only bytestrings by + default which coerce into unicode strings automatically. That behavior by + default is controlled by the ``JSON_AS_ASCII`` configuration variable + and can be overriden by the simplejson ``ensure_ascii`` parameter. """ - if current_app: - kwargs.setdefault('cls', current_app.json_encoder) + _dump_arg_defaults(kwargs) return _json.dumps(obj, **kwargs) def dump(obj, fp, **kwargs): """Like :func:`dumps` but writes into a file object.""" - if current_app: - kwargs.setdefault('cls', current_app.json_encoder) + _dump_arg_defaults(kwargs) return _json.dump(obj, fp, **kwargs) @@ -90,16 +107,14 @@ def loads(s, **kwargs): configured decoder (:attr:`~flask.Flask.json_decoder`) if there is an application on the stack. """ - if current_app: - kwargs.setdefault('cls', current_app.json_decoder) + _load_arg_defaults(kwargs) return _json.loads(s, **kwargs) def load(fp, **kwargs): """Like :func:`loads` but reads from a file object. """ - if current_app: - kwargs.setdefault('cls', current_app.json_decoder) + _load_arg_defaults(kwargs) return _json.load(fp, **kwargs) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index ee5312e0..31f0dcb4 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -79,6 +79,19 @@ class JSONTestCase(FlaskTestCase): self.assert_equal(rv.mimetype, 'application/json') self.assert_equal(flask.json.loads(rv.data), d) + def test_json_as_unicode(self): + app = flask.Flask(__name__) + + app.config['JSON_AS_ASCII'] = True + with app.app_context(): + rv = flask.json.dumps(u'\N{SNOWMAN}') + self.assert_equal(rv, '"\\u2603"') + + app.config['JSON_AS_ASCII'] = False + with app.app_context(): + rv = flask.json.dumps(u'\N{SNOWMAN}') + self.assert_equal(rv, u'"\u2603"') + def test_json_attr(self): app = flask.Flask(__name__) @app.route('/add', methods=['POST']) From af76dd0fd44301a6434d193626f8bf309eee8b26 Mon Sep 17 00:00:00 2001 From: Tony Narlock <tony@git-pull.com> Date: Fri, 19 Oct 2012 11:50:59 -0400 Subject: [PATCH 1134/3747] Highlight first field on page load, enter sends submit request, double quote to single quote. Submission focuses first field again --- examples/jqueryexample/templates/index.html | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/examples/jqueryexample/templates/index.html b/examples/jqueryexample/templates/index.html index 0545516d..55d01631 100644 --- a/examples/jqueryexample/templates/index.html +++ b/examples/jqueryexample/templates/index.html @@ -2,20 +2,32 @@ {% block body %} <script type=text/javascript> $(function() { - $('a#calculate').bind('click', function() { + var submit_form = function(e) { $.getJSON($SCRIPT_ROOT + '/_add_numbers', { a: $('input[name="a"]').val(), b: $('input[name="b"]').val() }, function(data) { - $("#result").text(data.result); + $('#result').text(data.result); + $('input[name=a]').focus().select(); }); return false; + }; + + $('a#calculate').bind('click', submit_form); + + $('input[type=text]').bind('keydown', function(e) { + if (e.keyCode == 13) { + submit_form(e); + } }); + + $('input[name=a]').focus(); }); </script> <h1>jQuery Example</h1> -<p><input type=text size=5 name=a> + - <input type=text size=5 name=b> = - <span id=result>?</span> +<p> + <input type=text size=5 name=a> + + <input type=text size=5 name=b> = + <span id=result>?</span> <p><a href=# id=calculate>calculate server side</a> {% endblock %} From 1e39871d681bd41451cf8694cd67fffe017fa522 Mon Sep 17 00:00:00 2001 From: Quentin Roy <royque@gmail.com> Date: Sat, 20 Oct 2012 18:18:42 +0200 Subject: [PATCH 1135/3747] Add back path=None argument into find_module. This fixes #618 and revert #524. --- scripts/flaskext_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index 050fd7e0..cb0b436c 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -44,7 +44,7 @@ class ExtensionImporter(object): def install(self): sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] - def find_module(self, fullname): + def find_module(self, fullname, path=None): if fullname.startswith(self.prefix): return self From 2b885ce4dc3f6b2ea2707a39a8198a84d7ad3991 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 30 Oct 2012 14:47:17 +0000 Subject: [PATCH 1136/3747] Added better error reporting for unicode errors in sessions --- flask/debughelpers.py | 6 ++++++ flask/sessions.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index edf8c111..3ebd2f3e 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -10,6 +10,12 @@ """ +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + class DebugFilesKeyError(KeyError, AssertionError): """Raised from request.files during debugging. The idea is that it can provide a better error message than just a generic KeyError/BadRequest. diff --git a/flask/sessions.py b/flask/sessions.py index 46ce0830..bbf41ba0 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -66,6 +66,14 @@ class TaggedJSONSerializer(object): return {' d': http_date(value)} elif isinstance(value, dict): return dict((k, _tag(v)) for k, v in value.iteritems()) + elif isinstance(value, str): + try: + return unicode(value) + except UnicodeError: + raise UnexpectedUnicodeError(u'A byte string with ' + u'non-ASCII data was passed to the session system ' + u'which can only store unicode strings. Consider ' + u'base64 encoding your string (String was %r)' % value) return value return json.dumps(_tag(value), separators=(',', ':')) @@ -292,3 +300,6 @@ class SecureCookieSessionInterface(SessionInterface): response.set_cookie(app.session_cookie_name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure) + + +from flask.debughelpers import UnexpectedUnicodeError From 3c54f30c2b761e3decd56aa7ab8e5d00493238a1 Mon Sep 17 00:00:00 2001 From: soulseekah <gennady@kovshenin.com> Date: Sat, 3 Nov 2012 18:06:23 +0600 Subject: [PATCH 1137/3747] missing ' in example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Results in SyntaxError: EOL while scanning string literal --- docs/templating.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templating.rst b/docs/templating.rst index 166f26aa..8a12cd6f 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -211,7 +211,7 @@ functions):: @app.context_processor def utility_processor(): def format_price(amount, currency=u'€'): - return u'{0:.2f}{1}.format(amount, currency) + return u'{0:.2f}{1}'.format(amount, currency) return dict(format_price=format_price) The context processor above makes the `format_price` function available to all From 82b29c09ac898042d589d852f1ef6f107fec9f71 Mon Sep 17 00:00:00 2001 From: Gennady Kovshenin <gennady@kovshenin.com> Date: Mon, 5 Nov 2012 06:00:46 +0600 Subject: [PATCH 1138/3747] Use sqlite3.Row factory in Flaskr As pointed out in issue #588 sqlite3.Row should be used instead of using casting to dict(). Also altered the "Easy Querying" Patterns example to include the more correct way to return rows as dicts. Did not touch Tutorial examples ("Views"), as these are not up to date with the current Flaskr code, and the "Show Entries" section points out the "Easy Querying" section on how to convert to a dict(). --- docs/patterns/sqlite3.rst | 4 ++++ examples/flaskr/flaskr.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 45bcc959..76fec0b2 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -76,6 +76,10 @@ dictionaries instead of tuples this can be used:: db.row_factory = make_dicts +Or even simpler:: + + db.row_factory = sqlite3.Row + Additionally it is a good idea to provide a query function that combines getting the cursor, executing and fetching the results:: diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 8526254d..0647fc7b 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -42,7 +42,10 @@ def get_db(): """ top = _app_ctx_stack.top if not hasattr(top, 'sqlite_db'): - top.sqlite_db = sqlite3.connect(app.config['DATABASE']) + sqlite_db = sqlite3.connect(app.config['DATABASE']) + sqlite_db.row_factory = sqlite3.Row + top.sqlite_db = sqlite_db + return top.sqlite_db @@ -58,7 +61,7 @@ def close_db_connection(exception): def show_entries(): db = get_db() cur = db.execute('select title, text from entries order by id desc') - entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()] + entries = cur.fetchall() return render_template('show_entries.html', entries=entries) From caefb67ccd0f8c1f5961c7e71f9658af98f9f009 Mon Sep 17 00:00:00 2001 From: Max Countryman <maxc@me.com> Date: Mon, 5 Nov 2012 15:31:07 -0800 Subject: [PATCH 1139/3747] correcting typo --- docs/patterns/streaming.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index ac232dcc..f5bff3ca 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -24,7 +24,7 @@ data and to then invoke that function and pass it to a response object:: yield ','.join(row) + '\n' return Response(generate(), mimetype='text/csv') -Each ``yield`` expression is directly sent to the browser. Now though +Each ``yield`` expression is directly sent to the browser. Note though that some WSGI middlewares might break streaming, so be careful there in debug environments with profilers and other things you might have enabled. From 30a51f2573423fce764b7f250e97e4b39471a480 Mon Sep 17 00:00:00 2001 From: Max Countryman <maxc@me.com> Date: Mon, 12 Nov 2012 16:58:38 -0800 Subject: [PATCH 1140/3747] correcting typo --- docs/appcontext.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index a6b67001..1a8ed73c 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -18,7 +18,7 @@ assumptions are true: modify it, there is no magic proxy that can give you a reference to the application object you're currently creating or modifying. -On the contrast, during request handling, a couple of other rules exist: +In the contrast, during request handling, a couple of other rules exist: - while a request is active, the context local objects (:data:`flask.request` and others) point to the current request. From b6dac3812f84359e6e9fb3d23e6ea55d6f87561c Mon Sep 17 00:00:00 2001 From: Max Countryman <maxc@me.com> Date: Mon, 12 Nov 2012 17:00:51 -0800 Subject: [PATCH 1141/3747] really fixing it this time --- docs/appcontext.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 1a8ed73c..12f08ff2 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -18,7 +18,7 @@ assumptions are true: modify it, there is no magic proxy that can give you a reference to the application object you're currently creating or modifying. -In the contrast, during request handling, a couple of other rules exist: +In contrast, during request handling, a couple of other rules exist: - while a request is active, the context local objects (:data:`flask.request` and others) point to the current request. From be1ed3327126edcb485be85a8f2de58f23b6ab64 Mon Sep 17 00:00:00 2001 From: Iyra Gaura <iyra72@gmail.com> Date: Sat, 17 Nov 2012 18:13:14 +0000 Subject: [PATCH 1142/3747] Ended your paragraph tag. --- examples/flaskr/templates/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/templates/login.html index 6f70bb76..25fdc614 100644 --- a/examples/flaskr/templates/login.html +++ b/examples/flaskr/templates/login.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %} <h2>Login</h2> - {% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %} + {% if error %}<p class=error><strong>Error:</strong> {{ error }}</p>{% endif %} <form action="{{ url_for('login') }}" method=post> <dl> <dt>Username: From 160aa8078166f7b32015c6b3ad63a91c4500badc Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer <markus@unterwaditzer.net> Date: Mon, 10 Dec 2012 18:37:03 +0100 Subject: [PATCH 1143/3747] Added hint about print statements in CGI. Fix #646 --- docs/deploying/cgi.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 1de9bd2c..17116be5 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -16,6 +16,10 @@ Engine`_, where execution happens in a CGI-like environment. not called because this will always start a local WSGI server which we do not want if we deploy that application to CGI / app engine. + With CGI, you will also have to make sure that your code does not contain + any ``print`` statements, or that ``sys.stdin`` is overridden by something + that doesn't write into the HTTP response. + Creating a `.cgi` file ---------------------- From a319516518bb39cb840529f41df713a6b4a6a563 Mon Sep 17 00:00:00 2001 From: Erik Rose <erik@mozilla.com> Date: Tue, 11 Dec 2012 14:11:27 -0800 Subject: [PATCH 1144/3747] Fix a typo in the deferred-callbacks docs. --- docs/patterns/deferredcallbacks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index 917c5125..feab2579 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -19,7 +19,7 @@ instead. Sometimes however moving that code there is just not a very pleasant experience or makes code look very awkward. As an alternative possibility you can attach a bunch of callback functions -to the :data:`~flask.g` object and call then at the end of the request. +to the :data:`~flask.g` object and call them at the end of the request. This way you can defer code execution from anywhere in the application. From be0b5196bf5b11130ca1688c1aab2750afba8433 Mon Sep 17 00:00:00 2001 From: INADA Naoki <songofacandy@gmail.com> Date: Thu, 13 Dec 2012 19:03:54 +0900 Subject: [PATCH 1145/3747] Update docs/extensiondev.rst Use `current_app` instead of `self.app`. --- docs/extensiondev.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index d266e1a2..0615e4db 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -165,6 +165,7 @@ The Extension Code Here's the contents of the `flask_sqlite3.py` for copy/paste:: import sqlite3 + from flask import current_app # Find the stack on which we want to store the database connection. # Starting with Flask 0.9, the _app_ctx_stack is the correct one, @@ -178,11 +179,9 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: class SQLite3(object): def __init__(self, app=None): + self.app = app if app is not None: - self.app = app - self.init_app(self.app) - else: - self.app = None + self.init_app(app) def init_app(self, app): app.config.setdefault('SQLITE3_DATABASE', ':memory:') @@ -194,7 +193,7 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: app.teardown_request(self.teardown) def connect(self): - return sqlite3.connect(self.app.config['SQLITE3_DATABASE']) + return sqlite3.connect(current_app.config['SQLITE3_DATABASE']) def teardown(self, exception): ctx = stack.top From 61d43c7f1269d9dafb40d4483cf88c7e32e95edc Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Wed, 19 Dec 2012 15:46:18 +0100 Subject: [PATCH 1146/3747] Removed unnecessary end-tags and fixed some broken html --- .../simple_page/templates/pages/hello.html | 4 +-- .../simple_page/templates/pages/index.html | 4 +-- .../simple_page/templates/pages/layout.html | 31 ++++++++----------- .../simple_page/templates/pages/world.html | 5 ++- examples/flaskr/templates/login.html | 2 +- 5 files changed, 20 insertions(+), 26 deletions(-) diff --git a/examples/blueprintexample/simple_page/templates/pages/hello.html b/examples/blueprintexample/simple_page/templates/pages/hello.html index 7fca6668..7e4a624d 100644 --- a/examples/blueprintexample/simple_page/templates/pages/hello.html +++ b/examples/blueprintexample/simple_page/templates/pages/hello.html @@ -1,5 +1,5 @@ {% extends "pages/layout.html" %} {% block body %} - Hello -{% endblock %} \ No newline at end of file + Hello +{% endblock %} diff --git a/examples/blueprintexample/simple_page/templates/pages/index.html b/examples/blueprintexample/simple_page/templates/pages/index.html index 0ca3ffe2..b8d92da4 100644 --- a/examples/blueprintexample/simple_page/templates/pages/index.html +++ b/examples/blueprintexample/simple_page/templates/pages/index.html @@ -1,5 +1,5 @@ {% extends "pages/layout.html" %} {% block body %} - Blueprint example page -{% endblock %} \ No newline at end of file + Blueprint example page +{% endblock %} diff --git a/examples/blueprintexample/simple_page/templates/pages/layout.html b/examples/blueprintexample/simple_page/templates/pages/layout.html index 2efccb95..e74a5871 100644 --- a/examples/blueprintexample/simple_page/templates/pages/layout.html +++ b/examples/blueprintexample/simple_page/templates/pages/layout.html @@ -3,23 +3,18 @@ <div class=page> <h1>This is blueprint example</h1> <p> - A simple page blueprint is registered under / and /pages<br/> - you can access it using this urls: - <ul> - <li><a href="{{ url_for('simple_page.show', page='hello') }}">/hello</a></li> - <li><a href="{{ url_for('simple_page.show', page='world') }}">/world</a></li> - </ul> - </p> + A simple page blueprint is registered under / and /pages + you can access it using this urls: + <ul> + <li><a href="{{ url_for('simple_page.show', page='hello') }}">/hello</a> + <li><a href="{{ url_for('simple_page.show', page='world') }}">/world</a> + </ul> <p> - Also you can register the same blueprint under another path - <ul> - <li><a href="/pages/hello">/pages/hello</a></li> - <li><a href="/pages/world">/pages/world</a></li> - </ul> - </p> - + Also you can register the same blueprint under another path + <ul> + <li><a href="/pages/hello">/pages/hello</a> + <li><a href="/pages/world">/pages/world</a> + </ul> - - {% block body %} - {% endblock %} -</div> \ No newline at end of file + {% block body %}{% endblock %} +</div> diff --git a/examples/blueprintexample/simple_page/templates/pages/world.html b/examples/blueprintexample/simple_page/templates/pages/world.html index bdb5b16b..9fa2880a 100644 --- a/examples/blueprintexample/simple_page/templates/pages/world.html +++ b/examples/blueprintexample/simple_page/templates/pages/world.html @@ -1,5 +1,4 @@ {% extends "pages/layout.html" %} - {% block body %} - World -{% endblock %} \ No newline at end of file + World +{% endblock %} diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/templates/login.html index 25fdc614..6f70bb76 100644 --- a/examples/flaskr/templates/login.html +++ b/examples/flaskr/templates/login.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %} <h2>Login</h2> - {% if error %}<p class=error><strong>Error:</strong> {{ error }}</p>{% endif %} + {% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %} <form action="{{ url_for('login') }}" method=post> <dl> <dt>Username: From c549e583c4cab9b657cf91544375031a64c79733 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer <markus@unterwaditzer.net> Date: Mon, 17 Dec 2012 21:32:59 +0100 Subject: [PATCH 1147/3747] Fixing my own pull request #647 Wrote "stdin" instead of "stdout". --- docs/deploying/cgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/cgi.rst b/docs/deploying/cgi.rst index 17116be5..3225bc58 100644 --- a/docs/deploying/cgi.rst +++ b/docs/deploying/cgi.rst @@ -17,7 +17,7 @@ Engine`_, where execution happens in a CGI-like environment. we do not want if we deploy that application to CGI / app engine. With CGI, you will also have to make sure that your code does not contain - any ``print`` statements, or that ``sys.stdin`` is overridden by something + any ``print`` statements, or that ``sys.stdout`` is overridden by something that doesn't write into the HTTP response. Creating a `.cgi` file From 1949c4a9abc174bf29620f6dd8ceab9ed3ace2eb Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Fri, 21 Dec 2012 11:45:42 +0100 Subject: [PATCH 1148/3747] flask.g is now on the app context and not the request context --- CHANGES | 5 +++++ docs/api.rst | 4 ++++ docs/upgrading.rst | 7 +++++++ flask/app.py | 22 +++++++++++++++++++--- flask/ctx.py | 11 +++++++++-- flask/globals.py | 16 ++++++++++++---- flask/templating.py | 15 ++++++++------- flask/testsuite/appctx.py | 6 +++--- flask/testsuite/basic.py | 5 ++++- 9 files changed, 71 insertions(+), 20 deletions(-) diff --git a/CHANGES b/CHANGES index 6a1ee7c7..ddba0928 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,11 @@ Release date to be decided. in less bytes being transmitted over the network. It's disabled by default to not cause confusion with existing libraries that might expect ``flask.json.dumps`` to return bytestrings by default. +- ``flask.g`` is now stored on the app context instead of the request + context. +- ``flask.Flask.request_globals_class`` got renamed to + ``flask.Flask.app_ctx_globals_class`` which is a better name to what it + does since 0.10. Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index dbd1877e..12e4b966 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -258,6 +258,10 @@ thing, like it does for :class:`request` and :class:`session`. Just store on this whatever you want. For example a database connection or the user that is currently logged in. + Starting with Flask 0.10 this is stored on the application context and + no longer on the request context which means it becomes available if + only the application context is bound and not yet a request. + This is a proxy. See :ref:`notes-on-proxies` for more information. diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 1f67df05..1d9239f5 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -36,6 +36,13 @@ extensions for tuples and strings with HTML markup. In order to not break people's sessions it is possible to continue using the old session system by using the `Flask-OldSessions_` extension. +Flask also started storing the :data:`flask.g` object on the application +context instead of the request context. This change should be transparent +for you but it means that you now can store things on the ``g`` object +when there is no request context yet but an application context. The old +``flask.Flask.request_globals_class`` attribute was renamed to +:attr:`flask.Flask.app_ctx_globals_class`. + .. _Flask-OldSessions: http://packages.python.org/Flask-OldSessions/ Version 0.9 diff --git a/flask/app.py b/flask/app.py index 9d291b3a..902c2ba8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -28,7 +28,7 @@ from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ from . import json from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import RequestContext, AppContext, _RequestGlobals +from .ctx import RequestContext, AppContext, _AppCtxGlobals from .globals import _request_ctx_stack, request from .sessions import SecureCookieSessionInterface from .module import blueprint_is_module @@ -157,8 +157,24 @@ class Flask(_PackageBoundObject): #: 3. Return None instead of AttributeError on expected attributes. #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. #: - #: .. versionadded:: 0.9 - request_globals_class = _RequestGlobals + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is not application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + # Backwards compatibility support + def _get_request_globals_class(self): + return self.app_ctx_globals_class + def _set_request_globals_class(self, value): + from warnings import warn + warn(DeprecationWarning('request_globals_class attribute is now ' + 'called app_ctx_globals_class')) + self.app_ctx_globals_class = value + request_globals_class = property(_get_request_globals_class, + _set_request_globals_class) + del _get_request_globals_class, _set_request_globals_class #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled diff --git a/flask/ctx.py b/flask/ctx.py index 84e96575..a1d9af62 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -17,7 +17,7 @@ from .globals import _request_ctx_stack, _app_ctx_stack from .module import blueprint_is_module -class _RequestGlobals(object): +class _AppCtxGlobals(object): """A plain object.""" pass @@ -101,6 +101,7 @@ class AppContext(object): def __init__(self, app): self.app = app self.url_adapter = app.create_url_adapter(None) + self.g = app.app_ctx_globals_class() # Like request context, app contexts can be pushed multiple times # but there a basic "refcount" is enough to track them. @@ -164,7 +165,6 @@ class RequestContext(object): self.app = app self.request = app.request_class(environ) self.url_adapter = app.create_url_adapter(self.request) - self.g = app.request_globals_class() self.flashes = None self.session = None @@ -195,6 +195,13 @@ class RequestContext(object): if bp is not None and blueprint_is_module(bp): self.request._is_old_module = True + def _get_g(self): + return _app_ctx_stack.top.g + def _set_g(self, value): + _app_ctx_stack.top.g = value + g = property(_get_g, _set_g) + del _get_g, _set_g + def match_request(self): """Can be overridden by a subclass to hook into the matching of the request. diff --git a/flask/globals.py b/flask/globals.py index f6d62485..67d41f5c 100644 --- a/flask/globals.py +++ b/flask/globals.py @@ -13,13 +13,21 @@ from functools import partial from werkzeug.local import LocalStack, LocalProxy -def _lookup_object(name): + +def _lookup_req_object(name): top = _request_ctx_stack.top if top is None: raise RuntimeError('working outside of request context') return getattr(top, name) +def _lookup_app_object(name): + top = _app_ctx_stack.top + if top is None: + raise RuntimeError('working outside of application context') + return getattr(top, name) + + def _find_app(): top = _app_ctx_stack.top if top is None: @@ -31,6 +39,6 @@ def _find_app(): _request_ctx_stack = LocalStack() _app_ctx_stack = LocalStack() current_app = LocalProxy(_find_app) -request = LocalProxy(partial(_lookup_object, 'request')) -session = LocalProxy(partial(_lookup_object, 'session')) -g = LocalProxy(partial(_lookup_object, 'g')) +request = LocalProxy(partial(_lookup_req_object, 'request')) +session = LocalProxy(partial(_lookup_req_object, 'session')) +g = LocalProxy(partial(_lookup_app_object, 'g')) diff --git a/flask/templating.py b/flask/templating.py index 2bac22e9..2cc09c4d 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -22,13 +22,14 @@ def _default_template_ctx_processor(): `session` and `g`. """ reqctx = _request_ctx_stack.top - if reqctx is None: - return {} - return dict( - request=reqctx.request, - session=reqctx.session, - g=reqctx.g - ) + appctx = _app_ctx_stack.top + rv = {} + if appctx is not None: + rv['g'] = appctx.g + if reqctx is not None: + rv['request'] = reqctx.request + rv['session'] = reqctx.session + return rv class Environment(BaseEnvironment): diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index 6454389e..aa71e11e 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -65,13 +65,13 @@ class AppContextTestCase(FlaskTestCase): self.assert_equal(cleanup_stuff, [None]) - def test_custom_request_globals_class(self): + def test_custom_app_ctx_globals_class(self): class CustomRequestGlobals(object): def __init__(self): self.spam = 'eggs' app = flask.Flask(__name__) - app.request_globals_class = CustomRequestGlobals - with app.test_request_context(): + app.app_ctx_globals_class = CustomRequestGlobals + with app.app_context(): self.assert_equal( flask.render_template_string('{{ g.spam }}'), 'eggs') diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 964d2c18..efee244a 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -1104,8 +1104,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): c.get('/fail') self.assert_(flask._request_ctx_stack.top is not None) - flask._request_ctx_stack.pop() + self.assert_(flask._app_ctx_stack.top is not None) + # implicit appctx disappears too + flask._request_ctx_stack.top.pop() self.assert_(flask._request_ctx_stack.top is None) + self.assert_(flask._app_ctx_stack.top is None) class ContextTestCase(FlaskTestCase): From 2af0ffaef63dbd6031d889d28e4d60794becb7b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Fri, 21 Dec 2012 11:47:27 +0100 Subject: [PATCH 1149/3747] Added proxies to template context --- CHANGES | 4 ++++ flask/app.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index ddba0928..044dae44 100644 --- a/CHANGES +++ b/CHANGES @@ -39,6 +39,10 @@ Release date to be decided. - ``flask.Flask.request_globals_class`` got renamed to ``flask.Flask.app_ctx_globals_class`` which is a better name to what it does since 0.10. +- `request`, `session` and `g` are now also added as proxies to the template + context which makes them available in imported templates. One has to be + very careful with those though because usage outside of macros might + cause caching. Version 0.9 ----------- diff --git a/flask/app.py b/flask/app.py index 902c2ba8..162a1d44 100644 --- a/flask/app.py +++ b/flask/app.py @@ -29,7 +29,7 @@ from . import json from .wrappers import Request, Response from .config import ConfigAttribute, Config from .ctx import RequestContext, AppContext, _AppCtxGlobals -from .globals import _request_ctx_stack, request +from .globals import _request_ctx_stack, request, session, g from .sessions import SecureCookieSessionInterface from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ @@ -663,7 +663,13 @@ class Flask(_PackageBoundObject): rv.globals.update( url_for=url_for, get_flashed_messages=get_flashed_messages, - config=self.config + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g ) rv.filters['tojson'] = json.htmlsafe_dumps return rv From cc82feb084092bf8212d0fb3a58386841f2294f6 Mon Sep 17 00:00:00 2001 From: oliversong <oliversong@tumblr.com> Date: Wed, 26 Dec 2012 00:05:18 -0500 Subject: [PATCH 1150/3747] Changing string to text in schema files --- docs/tutorial/schema.rst | 4 ++-- examples/flaskr/schema.sql | 4 ++-- examples/minitwit/schema.sql | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst index c078667e..e4aa2f59 100644 --- a/docs/tutorial/schema.rst +++ b/docs/tutorial/schema.rst @@ -13,8 +13,8 @@ the just created `flaskr` folder: drop table if exists entries; create table entries ( id integer primary key autoincrement, - title string not null, - text string not null + title text not null, + text text not null ); This schema consists of a single table called `entries` and each row in diff --git a/examples/flaskr/schema.sql b/examples/flaskr/schema.sql index 970cca77..dbb06319 100644 --- a/examples/flaskr/schema.sql +++ b/examples/flaskr/schema.sql @@ -1,6 +1,6 @@ drop table if exists entries; create table entries ( id integer primary key autoincrement, - title string not null, - text string not null + title text not null, + text text not null ); diff --git a/examples/minitwit/schema.sql b/examples/minitwit/schema.sql index b64afbed..b272adc8 100644 --- a/examples/minitwit/schema.sql +++ b/examples/minitwit/schema.sql @@ -1,9 +1,9 @@ drop table if exists user; create table user ( user_id integer primary key autoincrement, - username string not null, - email string not null, - pw_hash string not null + username text not null, + email text not null, + pw_hash text not null ); drop table if exists follower; @@ -16,6 +16,6 @@ drop table if exists message; create table message ( message_id integer primary key autoincrement, author_id integer not null, - text string not null, + text text not null, pub_date integer ); From e3e4f4c2b88ecc32babc714fcc345a978aeee2f0 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz <me@kennethreitz.com> Date: Mon, 31 Dec 2012 17:55:39 -0500 Subject: [PATCH 1151/3747] 2013 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index dc01ee1a..3c590329 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 by Armin Ronacher and contributors. See AUTHORS +Copyright (c) 2013 by Armin Ronacher and contributors. See AUTHORS for more details. Some rights reserved. From ff2e8449ffbb37cb8ebf4d7575c0e9102c78e772 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz <me@kennethreitz.com> Date: Mon, 31 Dec 2012 18:00:58 -0500 Subject: [PATCH 1152/3747] 2013 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 30df7147..c971a57d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2012, Armin Ronacher' +copyright = u'2013, Armin Ronacher' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 7a3d74f19f98b69e3047d0ed0085b5a1c9f806fc Mon Sep 17 00:00:00 2001 From: Baiju Muthukadan <baiju.m.mail@gmail.com> Date: Fri, 4 Jan 2013 14:37:13 +0530 Subject: [PATCH 1153/3747] WSGI specification has finalized for Python 3 Mention Python 3.x is not supported --- docs/installation.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 8dcae5a7..5e4673dd 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -14,9 +14,7 @@ could do that, but the most kick-ass method is virtualenv, so let's have a look at that first. You will need Python 2.5 or higher to get started, so be sure to have an -up-to-date Python 2.x installation. At the time of writing, the WSGI -specification has not yet been finalized for Python 3, so Flask cannot support -the 3.x series of Python. +up-to-date Python 2.x installation. Python 3.x is not supported. .. _virtualenv: From 2b30900e2c007cf39b3d73a8ad53afe2f29a3a2c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer <markus@unterwaditzer.net> Date: Thu, 8 Nov 2012 20:57:17 +0100 Subject: [PATCH 1154/3747] Fix #623 --- flask/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 6aea45c6..e8d72bf4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -247,8 +247,9 @@ def url_for(endpoint, **values): appctx = _app_ctx_stack.top reqctx = _request_ctx_stack.top if appctx is None: - raise RuntimeError('Attempted to generate a URL with the application ' - 'context being pushed. This has to be executed ') + raise RuntimeError('Attempted to generate a URL without the ' + 'application context being pushed. This has to be ' + 'executed when application context is available.') # If request specific information is available we have some extra # features that support "relative" urls. From b4fc4412e8bbdb359d1f5b9dd22a95326e73b896 Mon Sep 17 00:00:00 2001 From: Trung Ly <trungly@gmail.com> Date: Mon, 14 Jan 2013 14:53:06 -0800 Subject: [PATCH 1155/3747] Update docs/patterns/deferredcallbacks.rst fix example code in Deferred Callback docs: don't set response upon executing callback --- docs/patterns/deferredcallbacks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index feab2579..83d4fb49 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -49,7 +49,7 @@ this the following function needs to be registered as @app.after_request def call_after_request_callbacks(response): for callback in getattr(g, 'after_request_callbacks', ()): - response = callback(response) + callback(response) return response From f1537a9d7a1e5ab53e38166a2a8fd7bad9278c18 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 21 Jan 2013 17:44:32 +0000 Subject: [PATCH 1156/3747] Always trap proxy exceptions --- CHANGES | 2 ++ flask/app.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 044dae44..7f3374a1 100644 --- a/CHANGES +++ b/CHANGES @@ -43,6 +43,8 @@ Release date to be decided. context which makes them available in imported templates. One has to be very careful with those though because usage outside of macros might cause caching. +- Flask will no longer invoke the wrong error handlers if a proxy + exception is passed through. Version 0.9 ----------- diff --git a/flask/app.py b/flask/app.py index 162a1d44..5d87b1e8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1327,8 +1327,12 @@ class Flask(_PackageBoundObject): # ensure not to trash sys.exc_info() at that point in case someone # wants the traceback preserved in handle_http_exception. Of course # we cannot prevent users from trashing it themselves in a custom - # trap_http_exception method so that's their fault then. - if isinstance(e, HTTPException) and not self.trap_http_exception(e): + # trap_http_exception method so that's their fault then. Proxy + # exceptions generally must always be trapped (filtered out by + # e.code == None) + if isinstance(e, HTTPException) \ + and e.code is not None \ + and not self.trap_http_exception(e): return self.handle_http_exception(e) blueprint_handlers = () From 61d3bbf1d237cd8f03e37af88ad4a6b59f27d4e1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 21 Jan 2013 17:55:07 +0000 Subject: [PATCH 1157/3747] Fixed last commit and added test --- flask/app.py | 12 ++++++------ flask/testsuite/regression.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/flask/app.py b/flask/app.py index 5d87b1e8..f6d40be2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1283,6 +1283,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.3 """ handlers = self.error_handler_spec.get(request.blueprint) + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e if handlers and e.code in handlers: handler = handlers[e.code] else: @@ -1327,12 +1331,8 @@ class Flask(_PackageBoundObject): # ensure not to trash sys.exc_info() at that point in case someone # wants the traceback preserved in handle_http_exception. Of course # we cannot prevent users from trashing it themselves in a custom - # trap_http_exception method so that's their fault then. Proxy - # exceptions generally must always be trapped (filtered out by - # e.code == None) - if isinstance(e, HTTPException) \ - and e.code is not None \ - and not self.trap_http_exception(e): + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) blueprint_handlers = () diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index 87a6289b..34fdefc0 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -86,7 +86,32 @@ class MemoryTestCase(FlaskTestCase): safe_join('/foo', '..') +class ExceptionTestCase(FlaskTestCase): + + def test_aborting(self): + class Foo(Exception): + whatever = 42 + app = flask.Flask(__name__) + app.testing = True + @app.errorhandler(Foo) + def handle_foo(e): + return str(e.whatever) + @app.route('/') + def index(): + raise flask.abort(flask.redirect(flask.url_for('test'))) + @app.route('/test') + def test(): + raise Foo() + + with app.test_client() as c: + rv = c.get('/') + self.assertEqual(rv.headers['Location'], 'http://localhost/test') + rv = c.get('/test') + self.assertEqual(rv.data, '42') + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(MemoryTestCase)) + suite.addTest(unittest.makeSuite(ExceptionTestCase)) return suite From b5069d07a24a3c3a54fb056aa6f4076a0e7088c7 Mon Sep 17 00:00:00 2001 From: Max Countryman <max.countryman@gmail.com> Date: Thu, 17 Jan 2013 15:08:45 -0800 Subject: [PATCH 1158/3747] adding `_scheme` parameter to `url_for` In order to better facilitate generation of URLs that make use of an HTTPS URL scheme this patch adds a parameter with this specific purpose in mind. To achieve this we explicitly pass in a param, `_scheme='https'`, and then set the `url_scheme` attribute of our `MapAdapter` instance appropriately. Importantly, `_external=True` must be set in order for this to work properly. As such, failure to do so results in a `ValueError` being raised. --- flask/helpers.py | 12 ++++++++++++ flask/testsuite/helpers.py | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index 6aea45c6..a1c09cc5 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -229,6 +229,9 @@ def url_for(endpoint, **values): that this is for building URLs outside the current application, and not for handling 404 NotFound errors. + .. versionadded:: 0.10 + The `_scheme` parameter was added. + .. versionadded:: 0.9 The `_anchor` and `_method` parameters were added. @@ -241,6 +244,8 @@ def url_for(endpoint, **values): :param _external: if set to `True`, an absolute URL is generated. Server address can be changed via `SERVER_NAME` configuration variable which defaults to `localhost`. + :param _scheme: a string specifying the desired URL scheme. The `_external` + parameter must be set to `True` or a `ValueError` is raised. :param _anchor: if provided this is added as anchor to the URL. :param _method: if provided this explicitly specifies an HTTP method. """ @@ -283,7 +288,14 @@ def url_for(endpoint, **values): anchor = values.pop('_anchor', None) method = values.pop('_method', None) + scheme = values.pop('_scheme', None) appctx.app.inject_url_defaults(endpoint, values) + + if scheme is not None: + if not external: + raise ValueError('When specifying _scheme, _external must be True') + url_adapter.url_scheme = scheme + try: rv = url_adapter.build(endpoint, values, method=method, force_external=external) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 31f0dcb4..fdf2d89f 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -397,6 +397,28 @@ class LoggingTestCase(FlaskTestCase): self.assert_equal(flask.url_for('index', _anchor='x y'), '/#x%20y') + def test_url_for_with_scheme(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return '42' + with app.test_request_context(): + self.assert_equal(flask.url_for('index', + _external=True, + _scheme='https'), + 'https://localhost/') + + def test_url_for_with_scheme_not_external(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return '42' + with app.test_request_context(): + self.assert_raises(ValueError, + flask.url_for, + 'index', + _scheme='https') + def test_url_with_method(self): from flask.views import MethodView app = flask.Flask(__name__) From fb6dee3639b5fad1800472cb5258d2603408e732 Mon Sep 17 00:00:00 2001 From: schneems <richard.schneeman@gmail.com> Date: Thu, 24 Jan 2013 17:49:55 -0600 Subject: [PATCH 1159/3747] update README to markdown --- README | 55 ------------------------------------------------------ README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 7297f5db..00000000 --- a/README +++ /dev/null @@ -1,55 +0,0 @@ - // Flask // - - web development, one drop at a time - - - ~ What is Flask? - - Flask is a microframework for Python based on Werkzeug - and Jinja2. It's intended for getting started very quickly - and was developed with best intentions in mind. - - ~ Is it ready? - - It's still not 1.0 but it's shaping up nicely and is - already widely used. Consider the API to slightly - improve over time but we don't plan on breaking it. - - ~ What do I need? - - Jinja 2.4 and Werkzeug 0.7 or later. - `pip` or `easy_install` will install them for you if you do - `pip install Flask`. I encourage you to use a virtualenv. - Check the docs for complete installation and usage - instructions. - - ~ Where are the docs? - - Go to http://flask.pocoo.org/docs/ for a prebuilt version - of the current documentation. Otherwise build them yourself - from the sphinx sources in the docs folder. - - ~ Where are the tests? - - Good that you're asking. The tests are in the - flask/testsuite package. To run the tests use the - `run-tests.py` file: - - $ python run-tests.py - - If it's not enough output for you, you can use the - `--verbose` flag: - - $ python run-tests.py --verbose - - If you just want one particular testcase to run you can - provide it on the command line: - - $ python run-tests.py test_to_run - - ~ Where can I get help? - - Either use the #pocoo IRC channel on irc.freenode.net or - ask on the mailinglist: http://flask.pocoo.org/mailinglist/ - - See http://flask.pocoo.org/community/ for more resources. diff --git a/README.md b/README.md new file mode 100644 index 00000000..90084361 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Flask + +Web development, one drop at a time + + +## What is Flask? + +Flask is a microframework for Python based on Werkzeug +and Jinja2. It's intended for getting started very quickly +and was developed with best intentions in mind. + +## Is it ready? + +It's still not 1.0 but it's shaping up nicely and is +already widely used. Consider the API to slightly +improve over time but we don't plan on breaking it. + +## What do I need? + +Jinja 2.4 and Werkzeug 0.7 or later. +`pip` or `easy_install` will install them for you if you do +`pip install Flask`. I encourage you to use a virtualenv. +Check the docs for complete installation and usage +instructions. + +## Where are the docs? + +Go to http://flask.pocoo.org/docs/ for a prebuilt version +of the current documentation. Otherwise build them yourself +from the sphinx sources in the docs folder. + +## Where are the tests? + +Good that you're asking. The tests are in the +flask/testsuite package. To run the tests use the +`run-tests.py` file: + + $ python run-tests.py + +If it's not enough output for you, you can use the +`--verbose` flag: + + $ python run-tests.py --verbose + +If you just want one particular testcase to run you can +provide it on the command line: + + $ python run-tests.py test_to_run + +## Where can I get help? + +Either use the #pocoo IRC channel on irc.freenode.net or +ask on the mailinglist: http://flask.pocoo.org/mailinglist/ + +See http://flask.pocoo.org/community/ for more resources. + From 05f162329d425a3037c10cebfcdf941ce2802ca7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 27 Jan 2013 00:38:25 +0000 Subject: [PATCH 1160/3747] Added celery pattern --- docs/patterns/celery.rst | 92 ++++++++++++++++++++++++++++++++++++++++ docs/patterns/index.rst | 1 + 2 files changed, 93 insertions(+) create mode 100644 docs/patterns/celery.rst diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst new file mode 100644 index 00000000..c7cd3922 --- /dev/null +++ b/docs/patterns/celery.rst @@ -0,0 +1,92 @@ +Celery Based Background Tasks +============================= + +Celery is a task queue for Python with batteries included. It used to +have a Flask integration but it became unnecessary after some +restructuring of the internals of Celery with Version 3. This guide fills +in the blanks in how to properly use Celery with Flask but assumes that +you generally already read the `First Steps with Celery +<http://docs.celeryproject.org/en/master/getting-started/first-steps-with-celery.html>`_ +guide in the official Celery documentation. + +Installing Celery +----------------- + +Celery is on the Python Package Index (PyPI), so it can be installed with +standard Python tools like ``pip`` or ``easy_install``:: + + $ pip install celery + +Configuring Celery +------------------ + +The first thing you need is a Celery instance, this is called the celery +application. It serves the same purpose as the :class:`~flask.Flask` +object in Flask, just for Celery. Since this instance is used as the +entry-point for everything you want to do in Celery, like creating tasks +and managing workers, it must be possible for other modules to import it. + +For instance you can place this in a ``tasks`` module. While you can use +Celery without any reconfiguration with Flask, it becomes a bit nicer by +subclassing tasks and adding support for Flask's application contexts and +hooking it up with the Flask configuration. + +This is all that is necessary to properly integrate Celery with Flask:: + + from celery import Celery + + def make_celery(app): + celery = Celery(app.import_name, broker=app.config['CELERY_BROKER_URL']) + celery.conf.update(app.config) + TaskBase = celery.Task + class ContextTask(TaskBase): + abstract = True + def __call__(self, *args, **kwargs): + with app.app_context(): + return TaskBase.__call__(self, *args, **kwargs) + celery.Task = ContextTask + return celery + +The function creates a new Celery object, configures it with the broker +from the application config, updates the rest of the Celery config from +the Flask config and then creates a subclass of the task that wraps the +task execution in an application context. + +Minimal Example +--------------- + +With what we have above this is the minimal example of using Celery with +Flask:: + + from flask import Flask + + app = Flask(__name__) + app.config.update( + CELERY_BROKER_URL='redis://localhost:6379', + CELERY_RESULT_BACKEND='redis://localhost:6379' + ) + celery = make_celery(app) + + + @celery.task() + def add_together(a, b): + return a + b + +This task can now be called in the background: + +>>> result = add_together.delay(23, 42) +>>> result.wait() +65 + +Running the Celery Worker +------------------------- + +Now if you jumped in and already executed the above code you will be +disappointed to learn that your ``.wait()`` will never actually return. +That's because you also need to run celery. You can do that by running +celery as a worker:: + + $ celery -A your_application worker + +The ``your_application`` string has to point to your application's package +or module that creates the `celery` object. diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 4a09340f..8a9bf1ca 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -39,3 +39,4 @@ Snippet Archives <http://flask.pocoo.org/snippets/>`_. deferredcallbacks methodoverrides requestchecksum + celery From 3b393f89f66733f7824a50b33b93eae9999812ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 27 Jan 2013 00:46:19 +0000 Subject: [PATCH 1161/3747] Added template_global, fixes #657 --- CHANGES | 2 ++ flask/app.py | 32 ++++++++++++++++++++++++++++++++ flask/blueprints.py | 28 ++++++++++++++++++++++++++++ flask/testsuite/templating.py | 12 ++++++++++++ 4 files changed, 74 insertions(+) diff --git a/CHANGES b/CHANGES index 7f3374a1..eac9fad7 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,8 @@ Release date to be decided. :ref:`upgrading-to-010` for more information. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. +- Added ``template_global`` methods in addition to the already existing + ``template_filter`` method family. - Set the content-length header for x-sendfile. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. - Flask will now raise an error if you attempt to register a new function diff --git a/flask/app.py b/flask/app.py index f6d40be2..c9117727 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1165,6 +1165,38 @@ class Flask(_PackageBoundObject): self.jinja_env.tests[name or f.__name__] = f + @setupmethod + def template_global(self, name=None): + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + def decorator(f): + self.add_template_global(f, name=name) + return f + return decorator + + @setupmethod + def add_template_global(self, f, name=None): + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" diff --git a/flask/blueprints.py b/flask/blueprints.py index 7ce23bbc..4575ec9b 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -237,6 +237,34 @@ class Blueprint(_PackageBoundObject): state.app.jinja_env.tests[name or f.__name__] = f self.record_once(register_template) + def app_template_global(self, name=None): + """Register a custom template global, available application wide. Like + :meth:`Flask.template_global` but for a blueprint. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + def decorator(f): + self.add_app_template_global(f, name=name) + return f + return decorator + + def add_app_template_global(self, f, name=None): + """Register a custom template global, available application wide. Like + :meth:`Flask.add_template_global` but for a blueprint. Works exactly + like the :meth:`app_template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + def register_template(state): + state.app.jinja_env.globals[name or f.__name__] = f + self.record_once(register_template) + def before_request(self, f): """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 6345b710..635210f7 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -256,6 +256,18 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_('Success!' in rv.data) + def test_add_template_global(self): + app = flask.Flask(__name__) + @app.template_global() + def get_stuff(): + return 42 + self.assert_('get_stuff' in app.jinja_env.globals.keys()) + self.assert_equal(app.jinja_env.globals['get_stuff'], get_stuff) + self.assert_(app.jinja_env.globals['get_stuff'](), 42) + with app.app_context(): + rv = flask.render_template_string('{{ get_stuff() }}') + self.assert_equal(rv, '42') + def test_custom_template_loader(self): class MyFlask(flask.Flask): def create_global_jinja_loader(self): From 6ab569b0e3791cf04db7c333bbf5b78d33255dad Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Sun, 27 Jan 2013 00:56:01 +0000 Subject: [PATCH 1162/3747] Added note on teardown in debug mode. Fixes #661 --- flask/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask/app.py b/flask/app.py index c9117727..373479f5 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1250,6 +1250,13 @@ class Flask(_PackageBoundObject): When a teardown function was called because of a exception it will be passed an error object. + + .. admonition:: Debug Note + + In debug mode Flask will not tear down a request on an exception + immediately. Instead if will keep it alive so that the interactive + debugger can still access it. This behavior can be controlled + by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f From 6bd0080575a3413e349a20a658cc720809782238 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Mon, 28 Jan 2013 15:08:54 +0000 Subject: [PATCH 1163/3747] Added workaround for Chrome cookies --- CHANGES | 2 ++ flask/sessions.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index eac9fad7..33013bdd 100644 --- a/CHANGES +++ b/CHANGES @@ -47,6 +47,8 @@ Release date to be decided. cause caching. - Flask will no longer invoke the wrong error handlers if a proxy exception is passed through. +- Added a workaround for chrome's cookies in localhost not working + as intended with domain names. Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index bbf41ba0..ea2e999f 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -192,7 +192,13 @@ class SessionInterface(object): return app.config['SESSION_COOKIE_DOMAIN'] if app.config['SERVER_NAME'] is not None: # chop of the port which is usually not supported by browsers - return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + # Google chrome does not like cookies set to .localhost, so + # we just go with no domain then. Flask documents anyways that + # cross domain cookies need a fully qualified domain name + if rv == '.localhost': + rv = None + return rv def get_cookie_path(self, app): """Returns the path for which the cookie should be valid. The From bfeee756967ba1f6a74a6f2f4c3c617b4c0c4245 Mon Sep 17 00:00:00 2001 From: Armin Ronacher <armin.ronacher@active-4.com> Date: Tue, 29 Jan 2013 19:31:45 +0000 Subject: [PATCH 1164/3747] Changed session cookie defaults to work better with google chrome --- CHANGES | 2 ++ flask/sessions.py | 10 ++++++++++ flask/testsuite/basic.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGES b/CHANGES index 33013bdd..bbf51704 100644 --- a/CHANGES +++ b/CHANGES @@ -49,6 +49,8 @@ Release date to be decided. exception is passed through. - Added a workaround for chrome's cookies in localhost not working as intended with domain names. +- Changed logic for picking defaults for cookie values from sessions + to work better with Google Chrome. Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index ea2e999f..4a156d36 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -193,11 +193,21 @@ class SessionInterface(object): if app.config['SERVER_NAME'] is not None: # chop of the port which is usually not supported by browsers rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] + # Google chrome does not like cookies set to .localhost, so # we just go with no domain then. Flask documents anyways that # cross domain cookies need a fully qualified domain name if rv == '.localhost': rv = None + + # If we infer the cookie domain from the server name we need + # to check if we are in a subpath. In that case we can't + # set a cross domain cookie. + if rv is not None: + path = self.get_cookie_path(app) + if path != '/': + rv = rv.lstrip('.') + return rv def get_cookie_path(self, app): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index efee244a..aaf02fce 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -190,6 +190,22 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) self.assert_('httponly' in rv.headers['set-cookie'].lower()) + def test_session_using_server_name_port_and_path(self): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='foo', + SERVER_NAME='example.com:8080', + APPLICATION_ROOT='/foo' + ) + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + rv = app.test_client().get('/', 'http://example.com:8080/foo') + self.assert_('domain=example.com' in rv.headers['set-cookie'].lower()) + self.assert_('path=/foo' in rv.headers['set-cookie'].lower()) + self.assert_('httponly' in rv.headers['set-cookie'].lower()) + def test_session_using_application_root(self): class PrefixPathMiddleware(object): def __init__(self, app, prefix): From a754d28302b64dc9d86a5454b15d174b0115fa21 Mon Sep 17 00:00:00 2001 From: lambdadi <lambdadi@gmail.com> Date: Mon, 11 Feb 2013 18:55:24 +0530 Subject: [PATCH 1165/3747] Database improperly closed in example code os.unlink(flaskr.DATABASE) causes the actual application database to be purged; whereas, I reckon, one wants the _test_ database to be removed. So every time I ran it, the test passed, but ended up crashing the live app for want of a valid database. I avoided using the sample code in examples/flaskr thus far, as I chose to type out code from the turorial docs. The actual example code looks good - at least to my beginner's eye. --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index d4d9259c..84a6c277 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -102,7 +102,7 @@ test method to our class, like this:: def tearDown(self): os.close(self.db_fd) - os.unlink(flaskr.DATABASE) + os.unlink(flaskr.app.config['DATABASE']) def test_empty_db(self): rv = self.app.get('/') From c999c179a25998bfd52db359b243a07fdb8584c1 Mon Sep 17 00:00:00 2001 From: Devon Mizelle <dev@devon.so> Date: Sun, 17 Feb 2013 11:04:20 -0500 Subject: [PATCH 1166/3747] fixes #677 - mistype in docs/quickstart --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e9d6b388..cf5befa3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -695,7 +695,7 @@ Imagine you have a view like this: return render_template('error.html'), 404 You just need to wrap the return expression with -:func:`~flask.make_response` and get the result object to modify it, then +:func:`~flask.make_response` and get the response object to modify it, then return it: .. sourcecode:: python From a92588f161221dc1428eb3bc701ca79eb2a3ca11 Mon Sep 17 00:00:00 2001 From: Alex Couper <alex.couper@glassesdirect.com> Date: Fri, 22 Feb 2013 11:15:40 +0000 Subject: [PATCH 1167/3747] Add closing html tag --- docs/patterns/templateinheritance.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/patterns/templateinheritance.rst b/docs/patterns/templateinheritance.rst index 70015ecc..1292f26e 100644 --- a/docs/patterns/templateinheritance.rst +++ b/docs/patterns/templateinheritance.rst @@ -28,14 +28,15 @@ document that you might use for a simple two-column page. It's the job of <title>{% block title %}{% endblock %} - My Webpage {% endblock %} - -

    {% block content %}{% endblock %}
    - - + +
    {% block content %}{% endblock %}
    + + + In this example, the ``{% block %}`` tags define four blocks that child templates can fill in. All the `block` tag does is tell the template engine that a From cc8a85d7539bf7f1a6a0affd55f338e46af0d1c1 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 22 Feb 2013 11:21:17 +0000 Subject: [PATCH 1168/3747] Move docs explaining instantiating Flask --- docs/quickstart.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e9d6b388..082209b4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -38,15 +38,14 @@ should see your hello world greeting. So what did that code do? 1. First we imported the :class:`~flask.Flask` class. An instance of this - class will be our WSGI application. The first argument is the name of - the application's module. If you are using a single module (as in this - example), you should use `__name__` because depending on if it's started as - application or imported as module the name will be different (``'__main__'`` - versus the actual import name). For more information, have a look at the - :class:`~flask.Flask` documentation. -2. Next we create an instance of this class. We pass it the name of the module - or package. This is needed so that Flask knows where to look for templates, - static files, and so on. + class will be our WSGI application. +2. Next we create an instance of this class. The first argument is the name of + the application's module or package. If you are using a single module (as + in this example), you should use `__name__` because depending on if it's + started as application or imported as module the name will be different + (``'__main__'`` versus the actual import name). For more information, have + a look at the :class:`~flask.Flask` documentation. This is needed so that + Flask knows where to look for templates, static files, and so on. 3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask what URL should trigger our function. 4. The function is given a name which is also used to generate URLs for that From 8ee01ad5ed7296bb1030f3412c866b6fb39a92ad Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 22 Feb 2013 15:33:20 +0000 Subject: [PATCH 1169/3747] Put the link to Flask class docs at the end. --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 082209b4..01c6b833 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -43,9 +43,9 @@ So what did that code do? the application's module or package. If you are using a single module (as in this example), you should use `__name__` because depending on if it's started as application or imported as module the name will be different - (``'__main__'`` versus the actual import name). For more information, have - a look at the :class:`~flask.Flask` documentation. This is needed so that - Flask knows where to look for templates, static files, and so on. + (``'__main__'`` versus the actual import name). This is needed so that + Flask knows where to look for templates, static files, and so on. For more + information, have a look at the :class:`~flask.Flask` documentation. 3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask what URL should trigger our function. 4. The function is given a name which is also used to generate URLs for that From 1be13297c1518d01fb09bd65f7bfb6ca7449c424 Mon Sep 17 00:00:00 2001 From: Alex Couper Date: Fri, 22 Feb 2013 15:34:16 +0000 Subject: [PATCH 1170/3747] Remove comma. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 01c6b833..421235dc 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -45,7 +45,7 @@ So what did that code do? started as application or imported as module the name will be different (``'__main__'`` versus the actual import name). This is needed so that Flask knows where to look for templates, static files, and so on. For more - information, have a look at the :class:`~flask.Flask` documentation. + information have a look at the :class:`~flask.Flask` documentation. 3. We then use the :meth:`~flask.Flask.route` decorator to tell Flask what URL should trigger our function. 4. The function is given a name which is also used to generate URLs for that From b2aae044ca9c9875c594d57a3bd92bd80d8d56ff Mon Sep 17 00:00:00 2001 From: Paulo Poiati Date: Wed, 27 Feb 2013 23:46:17 -0300 Subject: [PATCH 1171/3747] Flash messages signals If we send a signal when a template is rendered why not when a message is flashed? One real world use case is in tests, this signal should make flash messages expectation easier to implement. --- flask/__init__.py | 2 +- flask/helpers.py | 3 +++ flask/signals.py | 1 + flask/testsuite/signals.py | 27 +++++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index 6e7883fb..52eec667 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -34,7 +34,7 @@ from .templating import render_template, render_template_string # the signals from .signals import signals_available, template_rendered, request_started, \ request_finished, got_request_exception, request_tearing_down, \ - appcontext_tearing_down + appcontext_tearing_down, message_flashed # We're not exposing the actual json module but a convenient wrapper around # it. diff --git a/flask/helpers.py b/flask/helpers.py index fe651004..d24dde6b 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -35,6 +35,7 @@ except ImportError: from jinja2 import FileSystemLoader +from .signals import message_flashed from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request @@ -361,6 +362,8 @@ def flash(message, category='message'): flashes = session.get('_flashes', []) flashes.append((category, message)) session['_flashes'] = flashes + message_flashed.send(current_app._get_current_object(), + message=message, category=category) def get_flashed_messages(with_categories=False, category_filter=[]): diff --git a/flask/signals.py b/flask/signals.py index 78a77bd5..14b728c6 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -50,3 +50,4 @@ request_finished = _signals.signal('request-finished') request_tearing_down = _signals.signal('request-tearing-down') got_request_exception = _signals.signal('got-request-exception') appcontext_tearing_down = _signals.signal('appcontext-tearing-down') +message_flashed = _signals.signal('message-flashed') diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index da1a68ca..0e5d0cea 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -8,6 +8,8 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import flask import unittest from flask.testsuite import FlaskTestCase @@ -95,6 +97,31 @@ class SignalsTestCase(FlaskTestCase): finally: flask.got_request_exception.disconnect(record, app) + def test_flash_signal(self): + app = flask.Flask(__name__) + app.config['SECRET_KEY'] = 'secret' + + @app.route('/') + def index(): + flask.flash('This is a flash message', category='notice') + return flask.redirect('/other') + + recorded = [] + def record(sender, message, category): + recorded.append((message, category)) + + flask.message_flashed.connect(record, app) + try: + client = app.test_client() + with client.session_transaction(): + client.get('/') + self.assert_equal(len(recorded), 1) + message, category = recorded[0] + self.assert_equal(message, 'This is a flash message') + self.assert_equal(category, 'notice') + finally: + flask.message_flashed.disconnect(record, app) + def suite(): suite = unittest.TestSuite() From cc5d6134c76d92134bc555706ca2d02af11e1d35 Mon Sep 17 00:00:00 2001 From: OrangeTux Date: Thu, 28 Feb 2013 11:12:16 +0100 Subject: [PATCH 1172/3747] Update css.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved consistency: lower cased all hexadecimal descriptions of colors. --- docs/tutorial/css.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst index 03f62ed1..b86225c4 100644 --- a/docs/tutorial/css.rst +++ b/docs/tutorial/css.rst @@ -10,7 +10,7 @@ folder we created before: .. sourcecode:: css body { font-family: sans-serif; background: #eee; } - a, h1, h2 { color: #377BA8; } + a, h1, h2 { color: #377ba8; } h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } @@ -24,8 +24,8 @@ folder we created before: .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } - .flash { background: #CEE5F5; padding: 0.5em; - border: 1px solid #AACBE2; } - .error { background: #F0D6D6; padding: 0.5em; } + .flash { background: #cee5F5; padding: 0.5em; + border: 1px solid #aacbe2; } + .error { background: #f0d6d6; padding: 0.5em; } Continue with :ref:`tutorial-testing`. From 8a6cba9e63567556468676b556fba43da6cba3f2 Mon Sep 17 00:00:00 2001 From: "Michael N. Gagnon" Date: Fri, 15 Mar 2013 07:31:00 -0700 Subject: [PATCH 1173/3747] fix typo in quickstart guide --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b30d1b64..f79a991f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -513,7 +513,7 @@ attributes mentioned above:: return log_the_user_in(request.form['username']) else: error = 'Invalid username/password' - # the code below this is executed if the request method + # the code below is executed if the request method # was GET or the credentials were invalid return render_template('login.html', error=error) From 9d8674d5b64db6ceab2bf331776a8900b8a9a636 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 16 Mar 2013 11:13:07 +0100 Subject: [PATCH 1174/3747] Added more information about app factories. --- docs/patterns/appfactories.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 2a6190ea..3ef80b42 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -30,6 +30,9 @@ The idea is to set up the application in a function. Like this:: app = Flask(__name__) app.config.from_pyfile(config_filename) + from yourapplication.model import db + db.init_app(app) + from yourapplication.views.admin import admin from yourapplication.views.frontend import frontend app.register_blueprint(admin) @@ -51,6 +54,21 @@ get access to the application with the config? Use Here we look up the name of a template in the config. +Extension objects are not initially bound to an application. Using +``db.init_app``, the app gets configured for the extension. No +application-specific state is stored on the extension object, so one extension +object can be used for multiple apps. For more information about the design of +extensions refer to :doc:`/extensiondev`. + +Your `model.py` might look like this when using `Flask-SQLAlchemy +`_:: + + from flask.ext.sqlalchemy import SQLAlchemy + # no app object passed! Instead we use use db.init_app in the factory. + db = SQLAlchemy() + + # create some models + Using Applications ------------------ From 4366bb392a5d41373d0a87c98a75eed59ceffd60 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 21 Mar 2013 21:04:06 +0000 Subject: [PATCH 1175/3747] Documented new signal message_flashed --- CHANGES | 1 + docs/api.rst | 8 ++++++++ docs/signals.rst | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/CHANGES b/CHANGES index bbf51704..ea7be9a7 100644 --- a/CHANGES +++ b/CHANGES @@ -51,6 +51,7 @@ Release date to be decided. as intended with domain names. - Changed logic for picking defaults for cookie values from sessions to work better with Google Chrome. +- Added `message_flashed` signal that simplifies flashing testing. Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 12e4b966..6e08241e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -524,6 +524,14 @@ Signals An `exc` keyword argument is passed with the exception that caused the teardown. +.. data:: message_flashed + + This signal is sent when the application is flashing a message. The + messages is sent as `message` keyword argument and the category as + `category`. + + .. versionadded:: 0.10 + .. currentmodule:: None .. class:: flask.signals.Namespace diff --git a/docs/signals.rst b/docs/signals.rst index 5f0af048..4d96cc14 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -291,4 +291,22 @@ The following signals exist in Flask: This will also be passed an `exc` keyword argument that has a reference to the exception that caused the teardown if there was one. +.. data:: flask.message_flashed + :noindex: + + This signal is sent when the application is flashing a message. The + messages is sent as `message` keyword argument and the category as + `category`. + + Example subscriber:: + + recorded = [] + def record(sender, message, category, **extra): + recorded.append((message, category)) + + from flask import message_flashed + message_flashed.connect(record, app) + + .. versionadded:: 0.10 + .. _blinker: http://pypi.python.org/pypi/blinker From 78ae0ec7f8d436df789d98c64e3579a196f52ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kalkosi=C5=84ski?= Date: Mon, 25 Mar 2013 12:29:28 +0100 Subject: [PATCH 1176/3747] Add import to jsonify example. --- flask/json.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/json.py b/flask/json.py index a85bdc63..f25b11b1 100644 --- a/flask/json.py +++ b/flask/json.py @@ -141,6 +141,8 @@ def jsonify(*args, **kwargs): to this function are the same as to the :class:`dict` constructor. Example usage:: + + from flask import jsonify @app.route('/_get_current_user') def get_current_user(): From 1723990aee6ef4587d851081c36b824418549266 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 26 Mar 2013 21:23:55 +0000 Subject: [PATCH 1177/3747] Fixed a few typos on quickstart --- docs/quickstart.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f79a991f..b455e070 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -54,7 +54,7 @@ So what did that code do? 5. Finally we use the :meth:`~flask.Flask.run` function to run the local server with our application. The ``if __name__ == '__main__':`` makes sure the server only runs if the script is executed directly from the Python - interpreter and not used as imported module. + interpreter and not used as an imported module. To stop the server, hit control-C. @@ -143,8 +143,8 @@ Variable Rules `````````````` To add variable parts to a URL you can mark these special sections as -````. Such a part is then passed as keyword argument to your -function. Optionally a converter can be specified by specifying a rule with +````. Such a part is then passed as a keyword argument to your +function. Optionally a converter can be used by specifying a rule with ````. Here are some nice examples:: @app.route('/user/') @@ -191,10 +191,10 @@ The following converters exist: rather like the pathname of a file on UNIX-like systems. Accessing the URL with a trailing slash will produce a 404 "Not Found" error. - This behavior allows relative URLs to continue working if users access the - page when they forget a trailing slash, consistent with how Apache - and other servers work. Also, the URLs will stay unique, which helps search - engines avoid indexing the same page twice. + This behavior allows relative URLs to continue working even if the trailing + slash is ommited, consistent with how Apache and other servers work. Also, + the URLs will stay unique, which helps search engines avoid indexing the + same page twice. .. _url-building: From 0a5d62f9ee44ccf6fed1584ef68b0c4c7d728b11 Mon Sep 17 00:00:00 2001 From: Ed Burnett Date: Tue, 26 Mar 2013 16:56:05 -0700 Subject: [PATCH 1178/3747] Updated jquery.rst with the current Google Developer hosted libraries URL and jquery version --- docs/patterns/jquery.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 72e401fc..1bd49533 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -31,11 +31,11 @@ to add a script statement to the bottom of your `` to load jQuery: url_for('static', filename='jquery.js') }}"> Another method is using Google's `AJAX Libraries API -`_ to load jQuery: +`_ to load jQuery: .. sourcecode:: html - + From 02150c0f2bf62fb980467e53f4f608d11f332f07 Mon Sep 17 00:00:00 2001 From: Ed Burnett Date: Tue, 26 Mar 2013 16:57:16 -0700 Subject: [PATCH 1179/3747] Added self to AUTHORS file --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 0f2a9827..22a4b3e8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Patches and Suggestions - Stephane Wirtel - Thomas Schranz - Zhao Xiaohong +- Edmond Burnett From b9355a7d5f79cfa06324a72cbaef4b9ec7a299d3 Mon Sep 17 00:00:00 2001 From: Steve Leonard Date: Thu, 4 Apr 2013 06:56:24 -0300 Subject: [PATCH 1180/3747] Mention register_error_handler in errorhandler doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documentation for @errorhandler gives "app.error_handler_spec[None][404] = page_not_found" as an example for adding an error handler without the decorator.  However, register_error_handler appears to be the correct way to do this (added 0.7), and it eliminates the problems with modifying error_handler_spec directly. --- flask/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flask/app.py b/flask/app.py index 373479f5..036903a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1065,6 +1065,11 @@ class Flask(_PackageBoundObject): The first `None` refers to the active blueprint. If the error handler should be application wide `None` shall be used. + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + .. versionadded:: 0.7 One can now additionally also register custom exception types that do not necessarily have to be a subclass of the From f74f446961f9fd8ecb2edffca53b82016cc41343 Mon Sep 17 00:00:00 2001 From: Trey Long Date: Thu, 4 Apr 2013 12:31:42 -0300 Subject: [PATCH 1181/3747] fixing process_response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flask.process_response will throw away functions is receives from ctx._after_request_functions if there is a Blueprint that has used @after_request. --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 373479f5..2da379f3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1679,7 +1679,7 @@ class Flask(_PackageBoundObject): bp = ctx.request.blueprint funcs = ctx._after_request_functions if bp is not None and bp in self.after_request_funcs: - funcs = reversed(self.after_request_funcs[bp]) + funcs = chain(funcs, reversed(self.after_request_funcs[bp])) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: From 8ca7fc47d0c9036f12ca628aabccb44a8dc3858d Mon Sep 17 00:00:00 2001 From: Alexander Thaller Date: Fri, 5 Apr 2013 13:48:08 +0300 Subject: [PATCH 1182/3747] Update README No need to manually initialize the database with a call to `init_db()` as this call is done before `app.run()` in flaskr.py, when you run the file. --- examples/flaskr/README | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/flaskr/README b/examples/flaskr/README index 9ab20589..9b5b9215 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -1,4 +1,3 @@ - / Flaskr / a minimal blog application @@ -14,11 +13,7 @@ export an FLASKR_SETTINGS environment variable pointing to a configuration file. - 2. fire up a python shell and run this: - - >>> from flaskr import init_db; init_db() - - 3. now you can run the flaskr.py file with your + 2. now you can run the flaskr.py file with your python interpreter and the application will greet you on http://localhost:5000/ From 0faed95385d731babac7d46a28e90ee25e702abd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 21 Mar 2013 21:04:06 +0000 Subject: [PATCH 1183/3747] Documented new signal message_flashed --- CHANGES | 1 + docs/api.rst | 8 ++++++++ docs/signals.rst | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/CHANGES b/CHANGES index bbf51704..ea7be9a7 100644 --- a/CHANGES +++ b/CHANGES @@ -51,6 +51,7 @@ Release date to be decided. as intended with domain names. - Changed logic for picking defaults for cookie values from sessions to work better with Google Chrome. +- Added `message_flashed` signal that simplifies flashing testing. Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 12e4b966..6e08241e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -524,6 +524,14 @@ Signals An `exc` keyword argument is passed with the exception that caused the teardown. +.. data:: message_flashed + + This signal is sent when the application is flashing a message. The + messages is sent as `message` keyword argument and the category as + `category`. + + .. versionadded:: 0.10 + .. currentmodule:: None .. class:: flask.signals.Namespace diff --git a/docs/signals.rst b/docs/signals.rst index 5f0af048..4d96cc14 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -291,4 +291,22 @@ The following signals exist in Flask: This will also be passed an `exc` keyword argument that has a reference to the exception that caused the teardown if there was one. +.. data:: flask.message_flashed + :noindex: + + This signal is sent when the application is flashing a message. The + messages is sent as `message` keyword argument and the category as + `category`. + + Example subscriber:: + + recorded = [] + def record(sender, message, category, **extra): + recorded.append((message, category)) + + from flask import message_flashed + message_flashed.connect(record, app) + + .. versionadded:: 0.10 + .. _blinker: http://pypi.python.org/pypi/blinker From d1bf82b0f4b180ac7523a7b7e63e7246039d4e71 Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Sun, 14 Apr 2013 00:10:38 -0700 Subject: [PATCH 1184/3747] Fixed typo in URL Processor documentation --- docs/patterns/urlprocessors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/urlprocessors.rst b/docs/patterns/urlprocessors.rst index 778a5a6b..3f65d758 100644 --- a/docs/patterns/urlprocessors.rst +++ b/docs/patterns/urlprocessors.rst @@ -65,7 +65,7 @@ dictionary and put it somewhere else:: def pull_lang_code(endpoint, values): g.lang_code = values.pop('lang_code', None) -That way you no longer have to do the `lang_code` assigment to +That way you no longer have to do the `lang_code` assignment to :data:`~flask.g` in every function. You can further improve that by writing your own decorator that prefixes URLs with the language code, but the more beautiful solution is using a blueprint. Once the From 1358fd9f3e0d1cdfa2a948a1176b276188ea959e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20Alt=C4=B1?= Date: Wed, 24 Apr 2013 09:42:52 +0300 Subject: [PATCH 1185/3747] Syntax highlighting for PyPI long description --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4a9182b9..2f9c95ca 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ intentions. And before you ask: It's BSD licensed! Flask is Fun ```````````` -:: +.. code:: python from flask import Flask app = Flask(__name__) @@ -23,7 +23,7 @@ Flask is Fun And Easy to Setup ````````````````` -:: +.. code:: bash $ pip install Flask $ python hello.py From a9b22af9bc77fb89047faa45090dc39631c2d0d5 Mon Sep 17 00:00:00 2001 From: Akshar Raaj Date: Fri, 3 May 2013 17:41:43 +0530 Subject: [PATCH 1186/3747] Fixed documentation in 'Design Decisions in Flask' --- docs/design.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design.rst b/docs/design.rst index ee83840e..f0f7126d 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -82,7 +82,7 @@ things (:ref:`app-factories`). The Routing System ------------------ -Flask uses the Werkzeug routing system which has was designed to +Flask uses the Werkzeug routing system which was designed to automatically order routes by complexity. This means that you can declare routes in arbitrary order and they will still work as expected. This is a requirement if you want to properly implement decorator based routing From 097353695e3178a38403b204ae4889c8a32bf997 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:00:04 +0100 Subject: [PATCH 1187/3747] Added flask.copy_current_request_context which simplies working with greenlets --- CHANGES | 2 + docs/api.rst | 2 + flask/__init__.py | 2 +- flask/app.py | 6 ++ flask/ctx.py | 77 +++++++++++++++- flask/testsuite/basic.py | 110 +--------------------- flask/testsuite/reqctx.py | 187 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 272 insertions(+), 114 deletions(-) create mode 100644 flask/testsuite/reqctx.py diff --git a/CHANGES b/CHANGES index ea7be9a7..e383b56f 100644 --- a/CHANGES +++ b/CHANGES @@ -52,6 +52,8 @@ Release date to be decided. - Changed logic for picking defaults for cookie values from sessions to work better with Google Chrome. - Added `message_flashed` signal that simplifies flashing testing. +- Added support for copying of request contexts for better working with + greenlets. Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 6e08241e..291bfabb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -280,6 +280,8 @@ Useful Functions and Classes .. autofunction:: has_request_context +.. autofunction:: copy_current_request_context + .. autofunction:: has_app_context .. autofunction:: url_for diff --git a/flask/__init__.py b/flask/__init__.py index 52eec667..978a4a4c 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -26,7 +26,7 @@ from .helpers import url_for, flash, send_file, send_from_directory, \ from .globals import current_app, g, request, session, _request_ctx_stack, \ _app_ctx_stack from .ctx import has_request_context, has_app_context, \ - after_this_request + after_this_request, copy_current_request_context from .module import Module from .blueprints import Blueprint from .templating import render_template, render_template_string diff --git a/flask/app.py b/flask/app.py index 373479f5..fba200e8 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1821,3 +1821,9 @@ class Flask(_PackageBoundObject): def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) + + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, + self.name, + ) diff --git a/flask/ctx.py b/flask/ctx.py index a1d9af62..6b271687 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -10,6 +10,7 @@ """ import sys +from functools import update_wrapper from werkzeug.exceptions import HTTPException @@ -19,7 +20,24 @@ from .module import blueprint_is_module class _AppCtxGlobals(object): """A plain object.""" - pass + + def __getitem__(self, name): + try: + return getattr(self, name) + except AttributeError: + return None + + def __setitem__(self, name, value): + setattr(self, name, value) + + def __delitem__(self, name, value): + delattr(self, name, value) + + def __repr__(self): + top = _app_ctx_stack.top + if top is not None: + return '' % top.app.name + return object.__repr__(self) def after_this_request(f): @@ -47,6 +65,41 @@ def after_this_request(f): return f +def copy_current_request_context(f): + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request like you + # would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + top = _request_ctx_stack.top + if top is None: + raise RuntimeError('This decorator can only be used at local scopes ' + 'when a request context is on the stack. For instance within ' + 'view functions.') + reqctx = top.copy() + def wrapper(*args, **kwargs): + with reqctx: + return f(*args, **kwargs) + return update_wrapper(wrapper, f) + + def has_request_context(): """If you have code that wants to test if a request context is there or not this function can be used. For instance, you may want to take advantage @@ -161,9 +214,11 @@ class RequestContext(object): that situation, otherwise your unittests will leak memory. """ - def __init__(self, app, environ): + def __init__(self, app, environ, request=None): self.app = app - self.request = app.request_class(environ) + if request is None: + request = app.request_class(environ) + self.request = request self.url_adapter = app.create_url_adapter(self.request) self.flashes = None self.session = None @@ -202,6 +257,20 @@ class RequestContext(object): g = property(_get_g, _set_g) del _get_g, _set_g + def copy(self): + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + """ + return self.__class__(self.app, + environ=self.request.environ, + request=self.request + ) + def match_request(self): """Can be overridden by a subclass to hook into the matching of the request. @@ -299,5 +368,5 @@ class RequestContext(object): self.__class__.__name__, self.request.url, self.request.method, - self.app.name + self.app.name, ) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index aaf02fce..445b6b41 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -666,19 +666,6 @@ class BasicFunctionalityTestCase(FlaskTestCase): else: self.fail('Expected exception') - def test_teardown_on_pop(self): - buffer = [] - app = flask.Flask(__name__) - @app.teardown_request - def end_of_request(exception): - buffer.append(exception) - - ctx = app.test_request_context() - ctx.push() - self.assert_equal(buffer, []) - ctx.pop() - self.assert_equal(buffer, [None]) - def test_response_creation(self): app = flask.Flask(__name__) @app.route('/unicode') @@ -821,53 +808,6 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(repr(flask.g), '') self.assertFalse(flask.g) - def test_proper_test_request_context(self): - app = flask.Flask(__name__) - app.config.update( - SERVER_NAME='localhost.localdomain:5000' - ) - - @app.route('/') - def index(): - return None - - @app.route('/', subdomain='foo') - def sub(): - return None - - with app.test_request_context('/'): - self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') - - with app.test_request_context('/'): - self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') - - try: - with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): - pass - except Exception, e: - self.assert_(isinstance(e, ValueError)) - self.assert_equal(str(e), "the server name provided " + - "('localhost.localdomain:5000') does not match the " + \ - "server name from the WSGI environment ('localhost')") - - try: - app.config.update(SERVER_NAME='localhost') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - - try: - app.config.update(SERVER_NAME='localhost:80') - with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): - pass - except ValueError, e: - raise ValueError( - "No ValueError exception should have been raised \"%s\"" % e - ) - def test_test_app_proper_environ(self): app = flask.Flask(__name__) app.config.update( @@ -1012,7 +952,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): values = dict() app.inject_url_defaults('foo.bar.baz.view', values) expected = dict(page='login') - self.assert_equal(values, expected) + self.assert_equal(values, expected) with app.test_request_context('/somepage'): url = flask.url_for('foo.bar.baz.view') @@ -1127,53 +1067,6 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_(flask._app_ctx_stack.top is None) -class ContextTestCase(FlaskTestCase): - - def test_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - @app.route('/meh') - def meh(): - return flask.request.url - - with app.test_request_context('/?name=World'): - self.assert_equal(index(), 'Hello World!') - with app.test_request_context('/meh'): - self.assert_equal(meh(), 'http://localhost/meh') - self.assert_(flask._request_ctx_stack.top is None) - - def test_context_test(self): - app = flask.Flask(__name__) - self.assert_(not flask.request) - self.assert_(not flask.has_request_context()) - ctx = app.test_request_context() - ctx.push() - try: - self.assert_(flask.request) - self.assert_(flask.has_request_context()) - finally: - ctx.pop() - - def test_manual_context_binding(self): - app = flask.Flask(__name__) - @app.route('/') - def index(): - return 'Hello %s!' % flask.request.args['name'] - - ctx = app.test_request_context('/?name=World') - ctx.push() - self.assert_equal(index(), 'Hello World!') - ctx.pop() - try: - index() - except RuntimeError: - pass - else: - self.assert_(0, 'expected runtime error') - - class SubdomainTestCase(FlaskTestCase): def test_basic_support(self): @@ -1251,6 +1144,5 @@ class SubdomainTestCase(FlaskTestCase): def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(BasicFunctionalityTestCase)) - suite.addTest(unittest.makeSuite(ContextTestCase)) suite.addTest(unittest.makeSuite(SubdomainTestCase)) return suite diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py new file mode 100644 index 00000000..a93523e7 --- /dev/null +++ b/flask/testsuite/reqctx.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +""" + flask.testsuite.reqctx + ~~~~~~~~~~~~~~~~~~~~~~ + + Tests the request context. + + :copyright: (c) 2012 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import with_statement + +import flask +import unittest +try: + from greenlet import greenlet +except ImportError: + greenlet = None +from flask.testsuite import FlaskTestCase + + +class RequestContextTestCase(FlaskTestCase): + + def test_teardown_on_pop(self): + buffer = [] + app = flask.Flask(__name__) + @app.teardown_request + def end_of_request(exception): + buffer.append(exception) + + ctx = app.test_request_context() + ctx.push() + self.assert_equal(buffer, []) + ctx.pop() + self.assert_equal(buffer, [None]) + + def test_proper_test_request_context(self): + app = flask.Flask(__name__) + app.config.update( + SERVER_NAME='localhost.localdomain:5000' + ) + + @app.route('/') + def index(): + return None + + @app.route('/', subdomain='foo') + def sub(): + return None + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/') + + with app.test_request_context('/'): + self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/') + + try: + with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): + pass + except Exception, e: + self.assert_(isinstance(e, ValueError)) + self.assert_equal(str(e), "the server name provided " + + "('localhost.localdomain:5000') does not match the " + \ + "server name from the WSGI environment ('localhost')") + + try: + app.config.update(SERVER_NAME='localhost') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + try: + app.config.update(SERVER_NAME='localhost:80') + with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): + pass + except ValueError, e: + raise ValueError( + "No ValueError exception should have been raised \"%s\"" % e + ) + + def test_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + @app.route('/meh') + def meh(): + return flask.request.url + + with app.test_request_context('/?name=World'): + self.assert_equal(index(), 'Hello World!') + with app.test_request_context('/meh'): + self.assert_equal(meh(), 'http://localhost/meh') + self.assert_(flask._request_ctx_stack.top is None) + + def test_context_test(self): + app = flask.Flask(__name__) + self.assert_(not flask.request) + self.assert_(not flask.has_request_context()) + ctx = app.test_request_context() + ctx.push() + try: + self.assert_(flask.request) + self.assert_(flask.has_request_context()) + finally: + ctx.pop() + + def test_manual_context_binding(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return 'Hello %s!' % flask.request.args['name'] + + ctx = app.test_request_context('/?name=World') + ctx.push() + self.assert_equal(index(), 'Hello World!') + ctx.pop() + try: + index() + except RuntimeError: + pass + else: + self.assert_(0, 'expected runtime error') + + def test_greenlet_context_copying(self): + app = flask.Flask(__name__) + greenlets = [] + + @app.route('/') + def index(): + reqctx = flask._request_ctx_stack.top.copy() + def g(): + self.assert_(not flask.request) + self.assert_(not flask.current_app) + with reqctx: + self.assert_(flask.request) + self.assert_equal(flask.current_app, app) + self.assert_equal(flask.request.path, '/') + self.assert_equal(flask.request.args['foo'], 'bar') + self.assert_(not flask.request) + return 42 + greenlets.append(greenlet(g)) + return 'Hello World!' + + rv = app.test_client().get('/?foo=bar') + self.assert_equal(rv.data, 'Hello World!') + + result = greenlets[0].run() + self.assert_equal(result, 42) + + def test_greenlet_context_copying_api(self): + app = flask.Flask(__name__) + greenlets = [] + + @app.route('/') + def index(): + reqctx = flask._request_ctx_stack.top.copy() + @flask.copy_current_request_context + def g(): + self.assert_(flask.request) + self.assert_equal(flask.current_app, app) + self.assert_equal(flask.request.path, '/') + self.assert_equal(flask.request.args['foo'], 'bar') + return 42 + greenlets.append(greenlet(g)) + return 'Hello World!' + + rv = app.test_client().get('/?foo=bar') + self.assert_equal(rv.data, 'Hello World!') + + result = greenlets[0].run() + self.assert_equal(result, 42) + + # Disable test if we don't have greenlets available + if greenlet is None: + test_greenlet_context_copying = None + test_greenlet_context_copying_api = None + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(RequestContextTestCase)) + return suite From 833198c91be327d23210115b15b5f6ce26bda91d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:23:31 +0100 Subject: [PATCH 1188/3747] Added a missing comma --- docs/templating.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templating.rst b/docs/templating.rst index 8a12cd6f..b6e1fc0a 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -188,7 +188,7 @@ you have a Python list in context called `mylist`:: Context Processors ------------------ -To inject new variables automatically into the context of a template +To inject new variables automatically into the context of a template, context processors exist in Flask. Context processors run before the template is rendered and have the ability to inject new values into the template context. A context processor is a function that returns a From 2ba37d2b85ac2c6cacb62055162e2e7b5e9cc55e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:33:13 +0100 Subject: [PATCH 1189/3747] Fixed some rst markup problems --- flask/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flask/app.py b/flask/app.py index fba200e8..831066fa 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1171,14 +1171,14 @@ class Flask(_PackageBoundObject): You can specify a name for the global function, otherwise the function name will be used. Example:: - @app.template_global() - def double(n): - return 2 * n + @app.template_global() + def double(n): + return 2 * n .. versionadded:: 0.10 :param name: the optional name of the global function, otherwise the - function name will be used. + function name will be used. """ def decorator(f): self.add_template_global(f, name=name) @@ -1193,7 +1193,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.10 :param name: the optional name of the global function, otherwise the - function name will be used. + function name will be used. """ self.jinja_env.globals[name or f.__name__] = f From 18673ba370e6744049aa2622b5973506d80659e0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:33:36 +0100 Subject: [PATCH 1190/3747] Added uuid support for new session serialization and documented it --- docs/api.rst | 12 ++++++++++++ flask/sessions.py | 5 +++++ flask/testsuite/basic.py | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 291bfabb..27333079 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -224,6 +224,18 @@ implementation that Flask is using. .. autoclass:: SessionMixin :members: +.. autodata:: session_json_serializer + + This object provides dumping and loading methods similar to simplejson + but it also tags certain builtin Python objects that commonly appear in + sessions. Currently the following extended values are supported in + the JSON it dumps: + + - :class:`~markupsafe.Markup` objects + - :class:`~uuid.UUID` objects + - :class:`~datetime.datetime` objects + - :class:`tuple`\s + .. admonition:: Notice The ``PERMANENT_SESSION_LIFETIME`` config key can also be an integer diff --git a/flask/sessions.py b/flask/sessions.py index 4a156d36..31b5900b 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ +import uuid import hashlib from datetime import datetime from werkzeug.http import http_date, parse_date @@ -58,6 +59,8 @@ class TaggedJSONSerializer(object): def _tag(value): if isinstance(value, tuple): return {' t': [_tag(x) for x in value]} + elif isinstance(value, uuid.UUID): + return {' u': value.hex} elif callable(getattr(value, '__html__', None)): return {' m': unicode(value.__html__())} elif isinstance(value, list): @@ -84,6 +87,8 @@ class TaggedJSONSerializer(object): the_key, the_value = obj.iteritems().next() if the_key == ' t': return tuple(the_value) + elif the_key == ' u': + return uuid.UUID(the_value) elif the_key == ' m': return Markup(the_value) elif the_key == ' d': diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 445b6b41..5f6dbe4b 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -12,6 +12,7 @@ from __future__ import with_statement import re +import uuid import flask import pickle import unittest @@ -319,10 +320,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.secret_key = 'development-key' app.testing = True now = datetime.utcnow().replace(microsecond=0) + the_uuid = uuid.uuid4() @app.after_request def modify_session(response): flask.session['m'] = flask.Markup('Hello!') + flask.session['u'] = the_uuid flask.session['dt'] = now flask.session['t'] = (1, 2, 3) return response @@ -337,6 +340,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(rv['m'], flask.Markup('Hello!')) self.assert_equal(type(rv['m']), flask.Markup) self.assert_equal(rv['dt'], now) + self.assert_equal(rv['u'], the_uuid) self.assert_equal(rv['t'], (1, 2, 3)) def test_flashes(self): From c349c91affd1d2123f50ab7a3d8c571da489338e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:35:45 +0100 Subject: [PATCH 1191/3747] Added support for UUID objects to JSON serializer as well --- flask/json.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flask/json.py b/flask/json.py index f25b11b1..717eb2ab 100644 --- a/flask/json.py +++ b/flask/json.py @@ -8,6 +8,7 @@ :copyright: (c) 2012 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import uuid from datetime import datetime from .globals import current_app, request @@ -30,9 +31,9 @@ __all__ = ['dump', 'dumps', 'load', 'loads', 'htmlsafe_dump', class JSONEncoder(_json.JSONEncoder): """The default Flask JSON encoder. This one extends the default simplejson - encoder by also supporting ``datetime`` objects as well as ``Markup`` - objects which are serialized as RFC 822 datetime strings (same as the HTTP - date format). In order to support more data types override the + encoder by also supporting ``datetime`` objects, ``UUID`` as well as + ``Markup`` objects which are serialized as RFC 822 datetime strings (same + as the HTTP date format). In order to support more data types override the :meth:`default` method. """ @@ -55,6 +56,8 @@ class JSONEncoder(_json.JSONEncoder): """ if isinstance(o, datetime): return http_date(o) + if isinstance(o, uuid.UUID): + return str(o) if hasattr(o, '__html__'): return unicode(o.__html__()) return _json.JSONEncoder.default(self, o) From 574a97cd701df65ef1b4aff3f3e0549d69ce5419 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 11:51:38 +0100 Subject: [PATCH 1192/3747] Disabled memory tests by default --- Makefile | 5 ++++- flask/testsuite/regression.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 773c680e..4b5e4fe2 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc ext-test test upload-docs docs audit +.PHONY: clean-pyc ext-test test test-with-mem upload-docs docs audit all: clean-pyc test test: python run-tests.py +test-with-mem: + RUN_FLASK_MEMORY_TESTS=1 python run-tests.py + audit: python setup.py audit diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index 34fdefc0..00219856 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -11,12 +11,12 @@ from __future__ import with_statement +import os import gc import sys import flask import threading import unittest -from werkzeug.test import run_wsgi_app, create_environ from werkzeug.exceptions import NotFound from flask.testsuite import FlaskTestCase @@ -112,6 +112,7 @@ class ExceptionTestCase(FlaskTestCase): def suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(MemoryTestCase)) + if os.environ.get('RUN_FLASK_MEMORY_TESTS') == '1': + suite.addTest(unittest.makeSuite(MemoryTestCase)) suite.addTest(unittest.makeSuite(ExceptionTestCase)) return suite From b89af66346538764bf506571ac8a7bfaf5d31083 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 14:05:34 +0100 Subject: [PATCH 1193/3747] Revert "update README to markdown" This reverts commit fb6dee3639b5fad1800472cb5258d2603408e732. --- README | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 56 ------------------------------------------------------- 2 files changed, 55 insertions(+), 56 deletions(-) create mode 100644 README delete mode 100644 README.md diff --git a/README b/README new file mode 100644 index 00000000..7297f5db --- /dev/null +++ b/README @@ -0,0 +1,55 @@ + // Flask // + + web development, one drop at a time + + + ~ What is Flask? + + Flask is a microframework for Python based on Werkzeug + and Jinja2. It's intended for getting started very quickly + and was developed with best intentions in mind. + + ~ Is it ready? + + It's still not 1.0 but it's shaping up nicely and is + already widely used. Consider the API to slightly + improve over time but we don't plan on breaking it. + + ~ What do I need? + + Jinja 2.4 and Werkzeug 0.7 or later. + `pip` or `easy_install` will install them for you if you do + `pip install Flask`. I encourage you to use a virtualenv. + Check the docs for complete installation and usage + instructions. + + ~ Where are the docs? + + Go to http://flask.pocoo.org/docs/ for a prebuilt version + of the current documentation. Otherwise build them yourself + from the sphinx sources in the docs folder. + + ~ Where are the tests? + + Good that you're asking. The tests are in the + flask/testsuite package. To run the tests use the + `run-tests.py` file: + + $ python run-tests.py + + If it's not enough output for you, you can use the + `--verbose` flag: + + $ python run-tests.py --verbose + + If you just want one particular testcase to run you can + provide it on the command line: + + $ python run-tests.py test_to_run + + ~ Where can I get help? + + Either use the #pocoo IRC channel on irc.freenode.net or + ask on the mailinglist: http://flask.pocoo.org/mailinglist/ + + See http://flask.pocoo.org/community/ for more resources. diff --git a/README.md b/README.md deleted file mode 100644 index 90084361..00000000 --- a/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Flask - -Web development, one drop at a time - - -## What is Flask? - -Flask is a microframework for Python based on Werkzeug -and Jinja2. It's intended for getting started very quickly -and was developed with best intentions in mind. - -## Is it ready? - -It's still not 1.0 but it's shaping up nicely and is -already widely used. Consider the API to slightly -improve over time but we don't plan on breaking it. - -## What do I need? - -Jinja 2.4 and Werkzeug 0.7 or later. -`pip` or `easy_install` will install them for you if you do -`pip install Flask`. I encourage you to use a virtualenv. -Check the docs for complete installation and usage -instructions. - -## Where are the docs? - -Go to http://flask.pocoo.org/docs/ for a prebuilt version -of the current documentation. Otherwise build them yourself -from the sphinx sources in the docs folder. - -## Where are the tests? - -Good that you're asking. The tests are in the -flask/testsuite package. To run the tests use the -`run-tests.py` file: - - $ python run-tests.py - -If it's not enough output for you, you can use the -`--verbose` flag: - - $ python run-tests.py --verbose - -If you just want one particular testcase to run you can -provide it on the command line: - - $ python run-tests.py test_to_run - -## Where can I get help? - -Either use the #pocoo IRC channel on irc.freenode.net or -ask on the mailinglist: http://flask.pocoo.org/mailinglist/ - -See http://flask.pocoo.org/community/ for more resources. - From 521398d5e79b4a2b910815a2870d39d8182a6cc2 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 May 2013 14:06:11 +0100 Subject: [PATCH 1194/3747] Added missing newlines to readme --- README | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README b/README index 7297f5db..9f7bed26 100644 --- a/README +++ b/README @@ -1,3 +1,5 @@ + + // Flask // web development, one drop at a time @@ -53,3 +55,5 @@ ask on the mailinglist: http://flask.pocoo.org/mailinglist/ See http://flask.pocoo.org/community/ for more resources. + + From 6caaa8a527134dc9aff6d5c442969e96f9c00f21 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 16:24:40 +0200 Subject: [PATCH 1195/3747] automated change using python-modernize: use 'as' in except --- flask/app.py | 10 +++++----- flask/config.py | 2 +- flask/ctx.py | 2 +- flask/debughelpers.py | 2 +- flask/helpers.py | 2 +- flask/testsuite/__init__.py | 2 +- flask/testsuite/basic.py | 20 ++++++++++---------- flask/testsuite/blueprints.py | 4 ++-- flask/testsuite/config.py | 8 ++++---- flask/testsuite/reqctx.py | 6 +++--- flask/testsuite/testing.py | 4 ++-- flask/wrappers.py | 2 +- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/flask/app.py b/flask/app.py index cc9a2f90..3204e13f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1478,7 +1478,7 @@ class Flask(_PackageBoundObject): rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() - except Exception, e: + except Exception as e: rv = self.handle_user_exception(e) response = self.make_response(rv) response = self.process_response(response) @@ -1516,9 +1516,9 @@ class Flask(_PackageBoundObject): methods = [] try: adapter.match(method='--') - except MethodNotAllowed, e: + except MethodNotAllowed as e: methods = e.valid_methods - except HTTPException, e: + except HTTPException as e: pass rv = self.response_class() rv.allow.update(methods) @@ -1626,7 +1626,7 @@ class Flask(_PackageBoundObject): rv = handler(error, endpoint, values) if rv is not None: return rv - except BuildError, error: + except BuildError as error: pass # At this point we want to reraise the exception. If the error is @@ -1807,7 +1807,7 @@ class Flask(_PackageBoundObject): with self.request_context(environ): try: response = self.full_dispatch_request() - except Exception, e: + except Exception as e: response = self.make_response(self.handle_exception(e)) return response(environ, start_response) diff --git a/flask/config.py b/flask/config.py index 759fd488..80e9983e 100644 --- a/flask/config.py +++ b/flask/config.py @@ -127,7 +127,7 @@ class Config(dict): d.__file__ = filename try: execfile(filename, d.__dict__) - except IOError, e: + except IOError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False e.strerror = 'Unable to load configuration file (%s)' % e.strerror diff --git a/flask/ctx.py b/flask/ctx.py index 6b271687..a02c9a86 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -279,7 +279,7 @@ class RequestContext(object): url_rule, self.request.view_args = \ self.url_adapter.match(return_rule=True) self.request.url_rule = url_rule - except HTTPException, e: + except HTTPException as e: self.request.routing_exception = e def push(self): diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 3ebd2f3e..504fab93 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -76,7 +76,7 @@ def attach_enctype_error_multidict(request): def __getitem__(self, key): try: return oldcls.__getitem__(self, key) - except KeyError, e: + except KeyError as e: if key not in request.form: raise raise DebugFilesKeyError(request, key) diff --git a/flask/helpers.py b/flask/helpers.py index d24dde6b..d68cb04b 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -301,7 +301,7 @@ def url_for(endpoint, **values): try: rv = url_adapter.build(endpoint, values, method=method, force_external=external) - except BuildError, error: + except BuildError as error: # We need to inject the values again so that the app callback can # deal with that sort of stuff. values['_external'] = external diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 5d86fa3d..084b3726 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -220,5 +220,5 @@ def main(): """Runs the testsuite as command line application.""" try: unittest.main(testLoader=BetterLoader(), defaultTest='suite') - except Exception, e: + except Exception as e: print 'Error: %s' % e diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 5f6dbe4b..2836a1b6 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -256,7 +256,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def expect_exception(f, *args, **kwargs): try: f(*args, **kwargs) - except RuntimeError, e: + except RuntimeError as e: self.assert_(e.args and 'session is unavailable' in e.args[0]) else: self.assert_(False, 'expected exception') @@ -629,7 +629,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() try: c.get('/fail') - except KeyError, e: + except KeyError as e: self.assert_(isinstance(e, BadRequest)) else: self.fail('Expected exception') @@ -645,7 +645,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() try: c.get('/fail') - except NotFound, e: + except NotFound as e: pass else: self.fail('Expected exception') @@ -664,7 +664,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): with app.test_client() as c: try: c.post('/fail', data={'foo': 'index.txt'}) - except DebugFilesKeyError, e: + except DebugFilesKeyError as e: self.assert_('no file contents were transmitted' in str(e)) self.assert_('This was submitted: "index.txt"' in str(e)) else: @@ -755,7 +755,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: with app.test_request_context(): flask.url_for('spam') - except BuildError, error: + except BuildError as error: pass try: raise RuntimeError('Test case where BuildError is not current.') @@ -802,7 +802,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return None try: app.test_client().get('/') - except ValueError, e: + except ValueError as e: self.assert_equal(str(e), 'View function did not return a response') pass else: @@ -843,7 +843,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = app.test_client().get('/', 'https://localhost.localdomain') # Werkzeug 0.8 self.assert_equal(rv.status_code, 404) - except ValueError, e: + except ValueError as e: # Werkzeug 0.7 self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:443') does not match the " + \ @@ -854,7 +854,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = app.test_client().get('/', 'http://foo.localhost') # Werkzeug 0.8 self.assert_equal(rv.status_code, 404) - except ValueError, e: + except ValueError as e: # Werkzeug 0.7 self.assert_equal(str(e), "the server name provided " + \ "('localhost.localdomain') does not match the " + \ @@ -975,7 +975,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/foo') def broken(): return 'Meh' - except AssertionError, e: + except AssertionError as e: self.assert_('A setup function was called' in str(e)) else: self.fail('Expected exception') @@ -1009,7 +1009,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): with app.test_client() as c: try: c.post('/foo', data={}) - except AssertionError, e: + except AssertionError as e: self.assert_('http://localhost/foo/' in str(e)) self.assert_('Make sure to directly send your POST-request ' 'to this URL' in str(e)) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index ea047918..0399d48e 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -183,7 +183,7 @@ class ModuleTestCase(FlaskTestCase): with app.test_request_context(): try: flask.render_template('missing.html') - except TemplateNotFound, e: + except TemplateNotFound as e: self.assert_equal(e.name, 'missing.html') else: self.assert_(0, 'expected exception') @@ -378,7 +378,7 @@ class BlueprintTestCase(FlaskTestCase): with app.test_request_context(): try: flask.render_template('missing.html') - except TemplateNotFound, e: + except TemplateNotFound as e: self.assert_equal(e.name, 'missing.html') else: self.assert_(0, 'expected exception') diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index bf72925b..0b2c65bd 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -57,7 +57,7 @@ class ConfigTestCase(FlaskTestCase): app = flask.Flask(__name__) try: app.config.from_envvar('FOO_SETTINGS') - except RuntimeError, e: + except RuntimeError as e: self.assert_("'FOO_SETTINGS' is not set" in str(e)) else: self.assert_(0, 'expected exception') @@ -76,7 +76,7 @@ class ConfigTestCase(FlaskTestCase): try: app = flask.Flask(__name__) app.config.from_envvar('FOO_SETTINGS') - except IOError, e: + except IOError as e: msg = str(e) self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' 'file (No such file or directory):')) @@ -91,7 +91,7 @@ class ConfigTestCase(FlaskTestCase): app = flask.Flask(__name__) try: app.config.from_pyfile('missing.cfg') - except IOError, e: + except IOError as e: msg = str(e) self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' 'file (No such file or directory):')) @@ -141,7 +141,7 @@ class InstanceTestCase(FlaskTestCase): here = os.path.abspath(os.path.dirname(__file__)) try: flask.Flask(__name__, instance_path='instance') - except ValueError, e: + except ValueError as e: self.assert_('must be absolute' in str(e)) else: self.fail('Expected value error') diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py index a93523e7..89a47728 100644 --- a/flask/testsuite/reqctx.py +++ b/flask/testsuite/reqctx.py @@ -58,7 +58,7 @@ class RequestContextTestCase(FlaskTestCase): try: with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): pass - except Exception, e: + except Exception as e: self.assert_(isinstance(e, ValueError)) self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:5000') does not match the " + \ @@ -68,7 +68,7 @@ class RequestContextTestCase(FlaskTestCase): app.config.update(SERVER_NAME='localhost') with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}): pass - except ValueError, e: + except ValueError as e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e ) @@ -77,7 +77,7 @@ class RequestContextTestCase(FlaskTestCase): app.config.update(SERVER_NAME='localhost:80') with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}): pass - except ValueError, e: + except ValueError as e: raise ValueError( "No ValueError exception should have been raised \"%s\"" % e ) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 92e3f267..605078a7 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -106,7 +106,7 @@ class TestToolsTestCase(FlaskTestCase): try: with c.session_transaction() as sess: pass - except RuntimeError, e: + except RuntimeError as e: self.assert_('Session backend did not open a session' in str(e)) else: self.fail('Expected runtime error') @@ -130,7 +130,7 @@ class TestToolsTestCase(FlaskTestCase): try: with c.session_transaction() as s: pass - except RuntimeError, e: + except RuntimeError as e: self.assert_('cookies' in str(e)) else: self.fail('Expected runtime error') diff --git a/flask/wrappers.py b/flask/wrappers.py index a56fe5d7..717d8ce5 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -101,7 +101,7 @@ class Request(RequestBase): if request_charset is not None: return json.loads(self.data, encoding=request_charset) return json.loads(self.data) - except ValueError, e: + except ValueError as e: return self.on_json_loading_failed(e) def on_json_loading_failed(self, e): From b52b7b1f9322e7ab3e69a387dbfe9fe14de4c401 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 16:31:40 +0200 Subject: [PATCH 1196/3747] automated change using python-modernize: replace execfile --- flask/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index 759fd488..55aef029 100644 --- a/flask/config.py +++ b/flask/config.py @@ -126,7 +126,7 @@ class Config(dict): d = imp.new_module('config') d.__file__ = filename try: - execfile(filename, d.__dict__) + exec(compile(open(filename).read(), filename, 'exec'), d.__dict__) except IOError, e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False From b8b769ad41edeb2320774d88502dd998df272397 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 16:39:39 +0200 Subject: [PATCH 1197/3747] automated change using python-modernize: fix methodattrs --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index cc9a2f90..b7826420 100644 --- a/flask/app.py +++ b/flask/app.py @@ -590,7 +590,7 @@ class Flask(_PackageBoundObject): # Hack to support the init_jinja_globals method which is supported # until 1.0 but has an API deficiency. if getattr(self.init_jinja_globals, 'im_func', None) is not \ - Flask.init_jinja_globals.im_func: + Flask.init_jinja_globals.__func__: from warnings import warn warn(DeprecationWarning('This flask class uses a customized ' 'init_jinja_globals() method which is deprecated. ' From 40fad2ece80e8bf6784e137028645fa66a3cd9c2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 17:06:25 +0200 Subject: [PATCH 1198/3747] document python 2.6 minimum requirement, remove all stuff that refers to 2.5 --- .travis.yml | 1 - CHANGES | 2 ++ docs/extensiondev.rst | 2 +- docs/installation.rst | 2 +- docs/patterns/jquery.rst | 4 ---- docs/tutorial/dbinit.rst | 7 ++----- flask/json.py | 3 +-- flask/wrappers.py | 2 -- scripts/flask-07-upgrade.py | 4 +--- setup.py | 1 - 10 files changed, 8 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd590bea..307945bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - 2.5 - 2.6 - 2.7 - pypy diff --git a/CHANGES b/CHANGES index e383b56f..af3c4752 100644 --- a/CHANGES +++ b/CHANGES @@ -54,6 +54,8 @@ Release date to be decided. - Added `message_flashed` signal that simplifies flashing testing. - Added support for copying of request contexts for better working with greenlets. +- Python requirements changed: requiring Python 2.6 or 2.7 now to prepare + for Python 3.3 port. Version 0.9 ----------- diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 0615e4db..09bf2d2c 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -390,7 +390,7 @@ extension to be approved you have to follow these guidelines: (``PackageName==dev``). 9. The ``zip_safe`` flag in the setup script must be set to ``False``, even if the extension would be safe for zipping. -10. An extension currently has to support Python 2.5, 2.6 as well as +10. An extension currently has to support Python 2.6 as well as Python 2.7 diff --git a/docs/installation.rst b/docs/installation.rst index 5e4673dd..16475383 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -13,7 +13,7 @@ So how do you get all that on your computer quickly? There are many ways you could do that, but the most kick-ass method is virtualenv, so let's have a look at that first. -You will need Python 2.5 or higher to get started, so be sure to have an +You will need Python 2.6 or higher to get started, so be sure to have an up-to-date Python 2.x installation. Python 3.x is not supported. .. _virtualenv: diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 1bd49533..7aaa2803 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -11,11 +11,7 @@ Python primitives (numbers, strings, dicts and lists) look like which is widely supported and very easy to parse. It became popular a few years ago and quickly replaced XML as transport format in web applications. -If you have Python 2.6 JSON will work out of the box, in Python 2.5 you -will have to install the `simplejson`_ library from PyPI. - .. _jQuery: http://jquery.com/ -.. _simplejson: http://pypi.python.org/pypi/simplejson Loading jQuery -------------- diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 6415cdaa..b32a8eda 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -21,12 +21,9 @@ errors. It's a good idea to add a function that initializes the database for you to the application. If you want to do that, you first have to import the -:func:`contextlib.closing` function from the contextlib package. If you -want to use Python 2.5 it's also necessary to enable the `with` statement -first (`__future__` imports must be the very first import). Accordingly, -add the following lines to your existing imports in `flaskr.py`:: +:func:`contextlib.closing` function from the contextlib package. +Accordingly, add the following lines to your existing imports in `flaskr.py`:: - from __future__ import with_statement from contextlib import closing Next we can create a function called `init_db` that initializes the diff --git a/flask/json.py b/flask/json.py index 717eb2ab..52e5680e 100644 --- a/flask/json.py +++ b/flask/json.py @@ -161,8 +161,7 @@ def jsonify(*args, **kwargs): "id": 42 } - This requires Python 2.6 or an installed version of simplejson. For - security reasons only objects are supported toplevel. For more + For security reasons only objects are supported toplevel. For more information about this, have a look at :ref:`json-security`. .. versionadded:: 0.2 diff --git a/flask/wrappers.py b/flask/wrappers.py index a56fe5d7..d348ee0a 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -92,8 +92,6 @@ class Request(RequestBase): def json(self): """If the mimetype is `application/json` this will contain the parsed JSON data. Otherwise this will be `None`. - - This requires Python 2.6 or an installed version of simplejson. """ if self.mimetype == 'application/json': request_charset = self.mimetype_params.get('charset') diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 4027d8ce..e1017e69 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -287,9 +287,7 @@ def main(): args = ['.'] if ast is None: - parser.error('Python 2.6 or later is required to run the upgrade script.\n' - 'The runtime requirements for Flask 0.7 however are still ' - 'Python 2.5.') + parser.error('Python 2.6 or later is required to run the upgrade script.') for path in args: scan_path(path, teardown=not options.no_teardown) diff --git a/setup.py b/setup.py index 2f9c95ca..ba5f4a67 100644 --- a/setup.py +++ b/setup.py @@ -101,7 +101,6 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', From 1b753cb1b1764de5bf3afe1f96032362208febc6 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 17:47:40 +0200 Subject: [PATCH 1199/3747] require 'six' in setup.py, add flask._compat for stuff not yet in 'six' --- flask/_compat.py | 13 +++++++++++++ setup.py | 1 + 2 files changed, 14 insertions(+) create mode 100644 flask/_compat.py diff --git a/flask/_compat.py b/flask/_compat.py new file mode 100644 index 00000000..3743a54c --- /dev/null +++ b/flask/_compat.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" + flask._compat + ~~~~~~~~~~~~~ + + Some py2/py3 compatibility support that is not yet available in + "six" 1.3.0. + There are bugs open for "six" for all this stuff, so we can remove it + again from here as soon as we require a new enough "six" release. + + :copyright: (c) 2013 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" diff --git a/setup.py b/setup.py index 2f9c95ca..e25d435f 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ setup( zip_safe=False, platforms='any', install_requires=[ + 'six>=1.3.0', 'Werkzeug>=0.7', 'Jinja2>=2.4', 'itsdangerous>=0.17' From cfbfff2d2692a94bdb29133c2e7bb6b792cb337d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 17:57:43 +0200 Subject: [PATCH 1200/3747] python-modernize automated changes: misc. minor stuff --- flask/testsuite/__init__.py | 3 ++- flask/testsuite/config.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 5d86fa3d..7938a784 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import print_function from __future__ import with_statement import os @@ -221,4 +222,4 @@ def main(): try: unittest.main(testLoader=BetterLoader(), defaultTest='suite') except Exception, e: - print 'Error: %s' % e + print('Error: %s' % e) diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index bf72925b..2027f578 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -113,7 +113,7 @@ class LimitedLoaderMockWrapper(object): def __getattr__(self, name): if name in ('archive', 'get_filename'): msg = 'Mocking a loader which does not have `%s.`' % name - raise AttributeError, msg + raise AttributeError(msg) return getattr(self.loader, name) From 0f8c47c988fbecc30785959065a53e00861c8558 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 18:00:15 +0200 Subject: [PATCH 1201/3747] python-modernize automated changes: fix_dict --- flask/sessions.py | 3 ++- flask/templating.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 31b5900b..3746da7f 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -17,6 +17,7 @@ from werkzeug.datastructures import CallbackDict from . import Markup, json from itsdangerous import URLSafeTimedSerializer, BadSignature +import six def total_seconds(td): @@ -68,7 +69,7 @@ class TaggedJSONSerializer(object): elif isinstance(value, datetime): return {' d': http_date(value)} elif isinstance(value, dict): - return dict((k, _tag(v)) for k, v in value.iteritems()) + return dict((k, _tag(v)) for k, v in six.iteritems(value)) elif isinstance(value, str): try: return unicode(value) diff --git a/flask/templating.py b/flask/templating.py index 2cc09c4d..754c6893 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -15,6 +15,7 @@ from jinja2 import BaseLoader, Environment as BaseEnvironment, \ from .globals import _request_ctx_stack, _app_ctx_stack from .signals import template_rendered from .module import blueprint_is_module +import six def _default_template_ctx_processor(): @@ -79,7 +80,7 @@ class DispatchingJinjaLoader(BaseLoader): except (ValueError, KeyError): pass - for blueprint in self.app.blueprints.itervalues(): + for blueprint in six.itervalues(self.app.blueprints): if blueprint_is_module(blueprint): continue loader = blueprint.jinja_loader @@ -92,7 +93,7 @@ class DispatchingJinjaLoader(BaseLoader): if loader is not None: result.update(loader.list_templates()) - for name, blueprint in self.app.blueprints.iteritems(): + for name, blueprint in six.iteritems(self.app.blueprints): loader = blueprint.jinja_loader if loader is not None: for template in loader.list_templates(): From dcd052366b43df7bfe730c2e991ff37c617ba52b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 18:03:37 +0200 Subject: [PATCH 1202/3747] python-modernize automated changes: fix_next --- flask/helpers.py | 3 ++- flask/sessions.py | 2 +- flask/testsuite/helpers.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index d24dde6b..a302995c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -26,6 +26,7 @@ from functools import update_wrapper from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound +import six # this was moved in 0.7 try: @@ -128,7 +129,7 @@ def stream_with_context(generator_or_function): # pushed. This item is discarded. Then when the iteration continues the # real generator is executed. wrapped_g = generator() - wrapped_g.next() + six.advance_iterator(wrapped_g) return wrapped_g diff --git a/flask/sessions.py b/flask/sessions.py index 3746da7f..b8e37014 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -85,7 +85,7 @@ class TaggedJSONSerializer(object): def object_hook(obj): if len(obj) != 1: return obj - the_key, the_value = obj.iteritems().next() + the_key, the_value = six.advance_iterator(obj.iteritems()) if the_key == ' t': return tuple(the_value) elif the_key == ' u': diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index fdf2d89f..5604f85f 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -18,6 +18,7 @@ from logging import StreamHandler from StringIO import StringIO from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr from werkzeug.http import parse_cache_control_header, parse_options_header +import six def has_encoding(name): @@ -507,7 +508,7 @@ class StreamingTestCase(FlaskTestCase): def close(self): called.append(42) def next(self): - return self._gen.next() + return six.advance_iterator(self._gen) @app.route('/') def index(): def generate(): From 522cd0009367d1f775b0e978681426773f51ce14 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 18:12:30 +0200 Subject: [PATCH 1203/3747] python-modernize automated changes: fix_unicode (but without six.u()) --- flask/json.py | 3 ++- flask/sessions.py | 4 ++-- flask/testsuite/basic.py | 3 ++- flask/testsuite/blueprints.py | 3 ++- flask/testsuite/helpers.py | 6 +++--- flask/testsuite/testing.py | 3 ++- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/flask/json.py b/flask/json.py index 717eb2ab..dee2e189 100644 --- a/flask/json.py +++ b/flask/json.py @@ -17,6 +17,7 @@ from werkzeug.http import http_date # Use the same json implementation as itsdangerous on which we # depend anyways. from itsdangerous import simplejson as _json +import six # figure out if simplejson escapes slashes. This behavior was changed @@ -59,7 +60,7 @@ class JSONEncoder(_json.JSONEncoder): if isinstance(o, uuid.UUID): return str(o) if hasattr(o, '__html__'): - return unicode(o.__html__()) + return six.text_type(o.__html__()) return _json.JSONEncoder.default(self, o) diff --git a/flask/sessions.py b/flask/sessions.py index b8e37014..04b7f8af 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -63,7 +63,7 @@ class TaggedJSONSerializer(object): elif isinstance(value, uuid.UUID): return {' u': value.hex} elif callable(getattr(value, '__html__', None)): - return {' m': unicode(value.__html__())} + return {' m': six.text_type(value.__html__())} elif isinstance(value, list): return [_tag(x) for x in value] elif isinstance(value, datetime): @@ -72,7 +72,7 @@ class TaggedJSONSerializer(object): return dict((k, _tag(v)) for k, v in six.iteritems(value)) elif isinstance(value, str): try: - return unicode(value) + return six.text_type(value) except UnicodeError: raise UnexpectedUnicodeError(u'A byte string with ' u'non-ASCII data was passed to the session system ' diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 5f6dbe4b..7cdf2211 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -22,6 +22,7 @@ from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import BadRequest, NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError +import six class BasicFunctionalityTestCase(FlaskTestCase): @@ -277,7 +278,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/test') def test(): - return unicode(flask.session.permanent) + return six.text_type(flask.session.permanent) client = app.test_client() rv = client.get('/') diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index ea047918..97426c8e 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -18,6 +18,7 @@ from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import NotFound from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound +import six # import moduleapp here because it uses deprecated features and we don't @@ -304,7 +305,7 @@ class BlueprintTestCase(FlaskTestCase): @bp.route('/bar') def bar(bar): - return unicode(bar) + return six.text_type(bar) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 5604f85f..ac618538 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -36,7 +36,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/json', methods=['POST']) def return_json(): - return unicode(flask.request.json) + return six.text_type(flask.request.json) c = app.test_client() rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) @@ -45,7 +45,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/json', methods=['POST']) def return_json(): - return unicode(flask.request.json) + return six.text_type(flask.request.json) c = app.test_client() rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) @@ -97,7 +97,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/add', methods=['POST']) def add(): - return unicode(flask.request.json['a'] + flask.request.json['b']) + return six.text_type(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 92e3f267..92be9d33 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -14,6 +14,7 @@ from __future__ import with_statement import flask import unittest from flask.testsuite import FlaskTestCase +import six class TestToolsTestCase(FlaskTestCase): @@ -85,7 +86,7 @@ class TestToolsTestCase(FlaskTestCase): @app.route('/') def index(): - return unicode(flask.session['foo']) + return six.text_type(flask.session['foo']) with app.test_client() as c: with c.session_transaction() as sess: From 323a840c5ab39e1df889d93ad0da28c5bd553c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 18 May 2013 18:27:49 +0200 Subject: [PATCH 1204/3747] Add tox.ini --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..56c8f393 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py26, py27, pypy, py33 + +[testenv] +commands = python run-tests.py [] From aba1d3a5074953702f1ddc9fe623d2a724dd6570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 18 May 2013 18:28:59 +0200 Subject: [PATCH 1205/3747] Test on 3.3 on travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 307945bf..f17e99b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - 2.6 - 2.7 - pypy + - 3.3 before_install: pip install simplejson From 287905e67c2f22df3c3055a0cca025931726cd02 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 18 May 2013 18:39:10 +0200 Subject: [PATCH 1206/3747] py3 compat: use six.reload_module --- flask/testsuite/ext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 034ab5be..096a70f8 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -13,7 +13,7 @@ from __future__ import with_statement import sys import unittest from flask.testsuite import FlaskTestCase - +from six import reload_module class ExtImportHookTestCase(FlaskTestCase): @@ -29,7 +29,7 @@ class ExtImportHookTestCase(FlaskTestCase): entry == 'flaskext') and value is not None: sys.modules.pop(entry, None) from flask import ext - reload(ext) + reload_module(ext) # reloading must not add more hooks import_hooks = 0 From c618db92d6059ab1262ea2450b0f81a94cdad901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 18 May 2013 18:55:36 +0200 Subject: [PATCH 1207/3747] reload_module is in six.moves --- flask/testsuite/ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 096a70f8..6f6a34f4 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -13,7 +13,7 @@ from __future__ import with_statement import sys import unittest from flask.testsuite import FlaskTestCase -from six import reload_module +from six.moves import reload_module class ExtImportHookTestCase(FlaskTestCase): From 506db0eab2bf9e514177d8b46e2708f10601cf36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 18 May 2013 19:00:06 +0200 Subject: [PATCH 1208/3747] Use print_function --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4ad0b546..1d3e36ad 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ Links `_ """ +from __future__ import print_function from setuptools import Command, setup class run_audit(Command): @@ -59,7 +60,7 @@ class run_audit(Command): try: import pyflakes.scripts.pyflakes as flakes except ImportError: - print "Audit requires PyFlakes installed in your system." + print("Audit requires PyFlakes installed in your system.") sys.exit(-1) warns = 0 @@ -71,9 +72,9 @@ class run_audit(Command): if file != '__init__.py' and file.endswith('.py') : warns += flakes.checkPath(os.path.join(root, file)) if warns > 0: - print "Audit finished with total %d warnings." % warns + print("Audit finished with total %d warnings." % warns) else: - print "No problems found in sourcecode." + print("No problems found in sourcecode.") setup( name='Flask', From ceb7c7f7717c13cdb9c6d2eccb135a9edc71eef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 18 May 2013 19:05:10 +0200 Subject: [PATCH 1209/3747] Don't notify IRC for this branch --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f17e99b8..33027fef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,9 +19,3 @@ notifications: # their own builder failure from us. Travis currently fails way # too many times by itself. email: false - - irc: - channels: - - "irc.freenode.org#pocoo" - use_notice: true - skip_join: true From 81c9b3570b70c092a2428fe9f21fb897ea5486b1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 20 May 2013 09:44:05 +0100 Subject: [PATCH 1210/3747] Removed 2.5 from travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fd590bea..307945bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - 2.5 - 2.6 - 2.7 - pypy From aecc41deb8d5cb8a1505be52648c78432e9238ae Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 20 May 2013 09:47:07 +0100 Subject: [PATCH 1211/3747] Restore 2.5 support for the time being --- .travis.yml | 1 + flask/ctx.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 307945bf..fd590bea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: + - 2.5 - 2.6 - 2.7 - pypy diff --git a/flask/ctx.py b/flask/ctx.py index 6b271687..0340486b 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -9,6 +9,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import sys from functools import update_wrapper From a503520ac51d16558ce91f7d7fc9d30a910d928c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 21 May 2013 23:34:25 +0200 Subject: [PATCH 1212/3747] copy _compat.py from flask in here (and adapt docstring) --- flask/_compat.py | 104 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/flask/_compat.py b/flask/_compat.py index 3743a54c..776a2dd7 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -3,11 +3,107 @@ flask._compat ~~~~~~~~~~~~~ - Some py2/py3 compatibility support that is not yet available in - "six" 1.3.0. - There are bugs open for "six" for all this stuff, so we can remove it - again from here as soon as we require a new enough "six" release. + Some py2/py3 compatibility support based on a stripped down + version of six so we don't have to depend on a specific version + of it. :copyright: (c) 2013 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import sys + +PY2 = sys.version_info[0] == 2 +PYPY = hasattr(sys, 'pypy_translation_info') +_identity = lambda x: x + + +if not PY2: + unichr = chr + range_type = range + text_type = str + string_types = (str,) + + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) + + import pickle + from io import BytesIO, StringIO + NativeStringIO = StringIO + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + ifilter = filter + imap = map + izip = zip + intern = sys.intern + + implements_iterator = _identity + implements_to_string = _identity + encode_filename = _identity + get_next = lambda x: x.__next__ + +else: + unichr = unichr + text_type = unicode + range_type = xrange + string_types = (str, unicode) + + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() + + import cPickle as pickle + from cStringIO import StringIO as BytesIO, StringIO + NativeStringIO = BytesIO + + exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') + + from itertools import imap, izip, ifilter + intern = intern + + def implements_iterator(cls): + cls.next = cls.__next__ + del cls.__next__ + return cls + + def implements_to_string(cls): + cls.__unicode__ = cls.__str__ + cls.__str__ = lambda x: x.__unicode__().encode('utf-8') + return cls + + get_next = lambda x: x.next + + def encode_filename(filename): + if isinstance(filename, unicode): + return filename.encode('utf-8') + return filename + + +def with_metaclass(meta, *bases): + # This requires a bit of explanation: the basic idea is to make a + # dummy metaclass for one level of class instanciation that replaces + # itself with the actual metaclass. Because of internal type checks + # we also need to make sure that we downgrade the custom metaclass + # for one level to something closer to type (that's why __call__ and + # __init__ comes back from type etc.). + # + # This has the advantage over six.with_metaclass in that it does not + # introduce dummy classes into the final MRO. + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +try: + from urllib.parse import quote_from_bytes as url_quote +except ImportError: + from urllib import quote as url_quote From e1d356fb713f3272db2a23f9f898c34c5dc79dc0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 22 May 2013 01:33:04 +0200 Subject: [PATCH 1213/3747] ported some more stuff to py 3.3 removed init_jinja_globals hack from app.py after consulting mitsuhiko (didn't work on py 3.3 "as is") removed with_statement future imports, not needed any more needs more work on 2.7 as well as on 3.3 --- examples/flaskr/flaskr.py | 2 +- examples/minitwit/minitwit.py | 2 +- flask/_compat.py | 6 ++++++ flask/app.py | 33 +++++++++------------------------ flask/config.py | 5 ++--- flask/exthook.py | 3 ++- flask/helpers.py | 9 ++++----- flask/testing.py | 4 +--- flask/testsuite/__init__.py | 5 ++--- flask/testsuite/appctx.py | 2 -- flask/testsuite/basic.py | 4 +--- flask/testsuite/blueprints.py | 2 -- flask/testsuite/config.py | 1 - flask/testsuite/deprecations.py | 19 +------------------ flask/testsuite/ext.py | 5 ++--- flask/testsuite/helpers.py | 4 +--- flask/testsuite/regression.py | 4 +--- flask/testsuite/reqctx.py | 2 -- flask/testsuite/signals.py | 1 - flask/testsuite/subclassing.py | 2 +- flask/testsuite/templating.py | 2 -- flask/testsuite/testing.py | 2 -- flask/testsuite/views.py | 2 +- scripts/flaskext_test.py | 2 -- 24 files changed, 36 insertions(+), 87 deletions(-) diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 0647fc7b..20254660 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -9,7 +9,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement + from sqlite3 import dbapi2 as sqlite3 from flask import Flask, request, session, g, redirect, url_for, abort, \ render_template, flash, _app_ctx_stack diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index a92da6af..2863de50 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -8,7 +8,7 @@ :copyright: (c) 2010 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement + import time from sqlite3 import dbapi2 as sqlite3 from hashlib import md5 diff --git a/flask/_compat.py b/flask/_compat.py index 776a2dd7..27f61137 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -22,6 +22,7 @@ if not PY2: range_type = range text_type = str string_types = (str,) + integer_types = (int, ) iterkeys = lambda d: iter(d.keys()) itervalues = lambda d: iter(d.values()) @@ -46,11 +47,14 @@ if not PY2: encode_filename = _identity get_next = lambda x: x.__next__ + from urllib.parse import urlparse + else: unichr = unichr text_type = unicode range_type = xrange string_types = (str, unicode) + integer_types = (int, long) iterkeys = lambda d: d.iterkeys() itervalues = lambda d: d.itervalues() @@ -82,6 +86,8 @@ else: return filename.encode('utf-8') return filename + from urlparse import urlparse + def with_metaclass(meta, *bases): # This requires a bit of explanation: the basic idea is to make a diff --git a/flask/app.py b/flask/app.py index 0337c608..dc684489 100644 --- a/flask/app.py +++ b/flask/app.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import os import sys from threading import Lock @@ -36,6 +34,7 @@ from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ request_tearing_down, appcontext_tearing_down +from flask._compat import reraise, string_types, integer_types # a lock used for logger initialization _logger_lock = Lock() @@ -585,21 +584,7 @@ class Flask(_PackageBoundObject): @locked_cached_property def jinja_env(self): """The Jinja2 environment used to load templates.""" - rv = self.create_jinja_environment() - - # Hack to support the init_jinja_globals method which is supported - # until 1.0 but has an API deficiency. - if getattr(self.init_jinja_globals, 'im_func', None) is not \ - Flask.init_jinja_globals.__func__: - from warnings import warn - warn(DeprecationWarning('This flask class uses a customized ' - 'init_jinja_globals() method which is deprecated. ' - 'Move the code from that method into the ' - 'create_jinja_environment() method instead.')) - self.__dict__['jinja_env'] = rv - self.init_jinja_globals() - - return rv + return self.create_jinja_environment() @property def got_first_request(self): @@ -1090,7 +1075,7 @@ class Flask(_PackageBoundObject): def _register_error_handler(self, key, code_or_exception, f): if isinstance(code_or_exception, HTTPException): code_or_exception = code_or_exception.code - if isinstance(code_or_exception, (int, long)): + if isinstance(code_or_exception, integer_types): assert code_or_exception != 500 or key is None, \ 'It is currently not possible to register a 500 internal ' \ 'server error on a per-blueprint level.' @@ -1137,7 +1122,7 @@ class Flask(_PackageBoundObject): def is_prime(n): if n == 2: return True - for i in xrange(2, int(math.ceil(math.sqrt(n))) + 1): + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): if n % i == 0: return False return True @@ -1383,7 +1368,7 @@ class Flask(_PackageBoundObject): if isinstance(e, typecheck): return handler(e) - raise exc_type, exc_value, tb + reraise(exc_type, exc_value, tb) def handle_exception(self, e): """Default exception handling that kicks in when an exception @@ -1405,7 +1390,7 @@ class Flask(_PackageBoundObject): # (the function was actually called from the except part) # otherwise, we just raise the error again if exc_value is e: - raise exc_type, exc_value, tb + reraise(exc_type, exc_value, tb) else: raise e @@ -1565,14 +1550,14 @@ class Flask(_PackageBoundObject): # set the headers and status. We do this because there can be # some extra logic involved when creating these objects with # specific values (like defualt content type selection). - if isinstance(rv, basestring): + if isinstance(rv, string_types): rv = self.response_class(rv, headers=headers, status=status) headers = status = None else: rv = self.response_class.force_type(rv, request.environ) if status is not None: - if isinstance(status, basestring): + if isinstance(status, string_types): rv.status = status else: rv.status_code = status @@ -1633,7 +1618,7 @@ class Flask(_PackageBoundObject): # still the same one we can reraise it with the original traceback, # otherwise we raise it from here. if error is exc_value: - raise exc_type, exc_value, tb + reraise(exc_type, exc_value, tb) raise error def preprocess_request(self): diff --git a/flask/config.py b/flask/config.py index 3afe623a..ddb113a5 100644 --- a/flask/config.py +++ b/flask/config.py @@ -9,13 +9,12 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import imp import os import errno from werkzeug.utils import import_string +from flask._compat import string_types class ConfigAttribute(object): @@ -158,7 +157,7 @@ class Config(dict): :param obj: an import name or object """ - if isinstance(obj, basestring): + if isinstance(obj, string_types): obj = import_string(obj) for key in dir(obj): if key.isupper(): diff --git a/flask/exthook.py b/flask/exthook.py index 26578f0f..89dac47b 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -21,6 +21,7 @@ """ import sys import os +from flask._compat import reraise class ExtensionImporter(object): @@ -77,7 +78,7 @@ class ExtensionImporter(object): # we swallow it and try the next choice. The skipped frame # is the one from __import__ above which we don't care about if self.is_important_traceback(realname, tb): - raise exc_type, exc_value, tb.tb_next + reraise(exc_type, exc_value, tb.tb_next) continue module = sys.modules[fullname] = sys.modules[realname] if '.' not in modname: diff --git a/flask/helpers.py b/flask/helpers.py index 68ebc74e..1359bba6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import os import sys import pkgutil @@ -27,6 +25,7 @@ from functools import update_wrapper from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound import six +from flask._compat import string_types, text_type # this was moved in 0.7 try: @@ -467,7 +466,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, :data:`~flask.current_app`. """ mtime = None - if isinstance(filename_or_fp, basestring): + if isinstance(filename_or_fp, string_types): filename = filename_or_fp file = None else: @@ -478,7 +477,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, # XXX: this behavior is now deprecated because it was unreliable. # removed in Flask 1.0 if not attachment_filename and not mimetype \ - and isinstance(filename, basestring): + and isinstance(filename, string_types): warn(DeprecationWarning('The filename support for file objects ' 'passed to send_file is now deprecated. Pass an ' 'attach_filename if you want mimetypes to be guessed.'), @@ -540,7 +539,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, os.path.getmtime(filename), os.path.getsize(filename), adler32( - filename.encode('utf-8') if isinstance(filename, unicode) + filename.encode('utf-8') if isinstance(filename, text_type) else filename ) & 0xffffffff )) diff --git a/flask/testing.py b/flask/testing.py index bdd3860f..ef116cf3 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,12 +10,10 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack -from urlparse import urlparse +from flask._compat import urlparse def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 9027369c..82fa3232 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -11,17 +11,16 @@ """ from __future__ import print_function -from __future__ import with_statement import os import sys import flask import warnings import unittest -from StringIO import StringIO from functools import update_wrapper from contextlib import contextmanager from werkzeug.utils import import_string, find_modules +from flask._compat import reraise, StringIO def add_to_path(path): @@ -159,7 +158,7 @@ class _ExceptionCatcher(object): self.test_case.fail('Expected exception of type %r' % exception_name) elif not issubclass(exc_type, self.exc_type): - raise exc_type, exc_value, tb + reraise(exc_type, exc_value, tb) return True diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index aa71e11e..afed923a 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest from flask.testsuite import FlaskTestCase diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 89159a16..974b4ebc 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import re import uuid import flask @@ -1060,7 +1058,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1/0 c = app.test_client() - for x in xrange(3): + for x in range(3): with self.assert_raises(ZeroDivisionError): c.get('/fail') diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index eba43e63..331854a6 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest import warnings diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index ca142cff..415d8aca 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -8,7 +8,6 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement import os import sys diff --git a/flask/testsuite/deprecations.py b/flask/testsuite/deprecations.py index 795a5d3d..56371822 100644 --- a/flask/testsuite/deprecations.py +++ b/flask/testsuite/deprecations.py @@ -9,30 +9,13 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest from flask.testsuite import FlaskTestCase, catch_warnings class DeprecationsTestCase(FlaskTestCase): - - def test_init_jinja_globals(self): - class MyFlask(flask.Flask): - def init_jinja_globals(self): - self.jinja_env.globals['foo'] = '42' - - with catch_warnings() as log: - app = MyFlask(__name__) - @app.route('/') - def foo(): - return app.jinja_env.globals['foo'] - - c = app.test_client() - self.assert_equal(c.get('/').data, '42') - self.assert_equal(len(log), 1) - self.assert_('init_jinja_globals' in str(log[0]['message'])) + """not used currently""" def suite(): diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 6f6a34f4..147e23b0 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -8,7 +8,6 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement import sys import unittest @@ -22,7 +21,7 @@ class ExtImportHookTestCase(FlaskTestCase): # that a real flaskext could be in there which would disable our # fake package. Secondly we want to make sure that the flaskext # import hook does not break on reloading. - for entry, value in sys.modules.items(): + for entry, value in list(sys.modules.items()): if (entry.startswith('flask.ext.') or entry.startswith('flask_') or entry.startswith('flaskext.') or @@ -100,7 +99,7 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_equal(test_function(), 42) def test_flaskext_broken_package_no_module_caching(self): - for x in xrange(2): + for x in range(2): with self.assert_raises(ImportError): import flask.ext.broken diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index ac618538..2808a409 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -9,16 +9,14 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import os import flask import unittest from logging import StreamHandler -from StringIO import StringIO from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr from werkzeug.http import parse_cache_control_header, parse_options_header import six +from flask._compat import StringIO def has_encoding(name): diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index 00219856..b8140d6f 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import os import gc import sys @@ -77,7 +75,7 @@ class MemoryTestCase(FlaskTestCase): if sys.version_info >= (2, 7) and \ not hasattr(sys, 'pypy_translation_info'): with self.assert_no_leak(): - for x in xrange(10): + for x in range(10): fire() def test_safe_join_toplevel_pardir(self): diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py index 89a47728..0f0282e5 100644 --- a/flask/testsuite/reqctx.py +++ b/flask/testsuite/reqctx.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest try: diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index 0e5d0cea..637c2c66 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -8,7 +8,6 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement import flask import unittest diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index 89aa9150..3e8d75c1 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -11,9 +11,9 @@ """ import flask import unittest -from StringIO import StringIO from logging import StreamHandler from flask.testsuite import FlaskTestCase +from flask._compat import StringIO class FlaskSubclassingTestCase(FlaskTestCase): diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 635210f7..c8d1dd22 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest from flask.testsuite import FlaskTestCase diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 2872ecdc..a37f6e6c 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import flask import unittest from flask.testsuite import FlaskTestCase diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index f09c1266..6c3ae816 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -8,7 +8,7 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement + import flask import flask.views import unittest diff --git a/scripts/flaskext_test.py b/scripts/flaskext_test.py index d1d5d991..5b0d9d22 100644 --- a/scripts/flaskext_test.py +++ b/scripts/flaskext_test.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for more details. """ -from __future__ import with_statement - import os import sys import shutil From da5edad23a13336d218ee08f9b8843f611aded07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 16:19:57 +0200 Subject: [PATCH 1214/3747] Use werkzeug@sprint-branch in tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 56c8f393..161e86fb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,5 @@ envlist = py26, py27, pypy, py33 [testenv] +deps = -egit+git://github.com/mitsuhiko/werkzeug.git@sprint-branch#egg=werkzeug commands = python run-tests.py [] From 884aad8ecebe309d3b17502ecaa64008d2b461e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 16:33:05 +0200 Subject: [PATCH 1215/3747] Test using itsdangerous with 3.x support --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 161e86fb..c699ac48 100644 --- a/tox.ini +++ b/tox.ini @@ -3,4 +3,5 @@ envlist = py26, py27, pypy, py33 [testenv] deps = -egit+git://github.com/mitsuhiko/werkzeug.git@sprint-branch#egg=werkzeug + -egit+git://github.com/mitsuhiko/itsdangerous.git#egg=itsdangerous commands = python run-tests.py [] From 3f51a09db4673c45f19cf1dcdbe04212ac7de890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 16:33:50 +0200 Subject: [PATCH 1216/3747] itsdangerous uses json instead of simplejson now --- flask/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/json.py b/flask/json.py index d5da4f91..6b95fde0 100644 --- a/flask/json.py +++ b/flask/json.py @@ -16,7 +16,7 @@ from werkzeug.http import http_date # Use the same json implementation as itsdangerous on which we # depend anyways. -from itsdangerous import simplejson as _json +from itsdangerous import json as _json import six From 05f66ad73543db207791ea5d6a2de2e8eaaf1640 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 22 May 2013 17:05:48 +0200 Subject: [PATCH 1217/3747] Fix some literals --- flask/testsuite/basic.py | 111 +++++++++++++++++----------------- flask/testsuite/blueprints.py | 74 +++++++++++------------ flask/testsuite/helpers.py | 16 ++--- flask/testsuite/regression.py | 4 +- flask/testsuite/reqctx.py | 4 +- flask/testsuite/signals.py | 4 +- flask/testsuite/templating.py | 16 ++--- flask/testsuite/testing.py | 8 +-- flask/testsuite/views.py | 14 ++--- 9 files changed, 125 insertions(+), 126 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 974b4ebc..1636bee7 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -32,7 +32,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'Hello World' rv = app.test_client().open('/', method='OPTIONS') self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) - self.assert_equal(rv.data, '') + self.assert_equal(rv.data, b'') def test_options_on_multiple_rules(self): app = flask.Flask(__name__) @@ -72,15 +72,15 @@ class BasicFunctionalityTestCase(FlaskTestCase): return flask.request.method c = app.test_client() - self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.get('/').data, b'GET') rv = c.post('/') self.assert_equal(rv.status_code, 405) self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) self.assert_(not rv.data) # head truncates - self.assert_equal(c.post('/more').data, 'POST') - self.assert_equal(c.get('/more').data, 'GET') + self.assert_equal(c.post('/more').data, b'POST') + self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') self.assert_equal(rv.status_code, 405) self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) @@ -96,15 +96,15 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) c = app.test_client() - self.assert_equal(c.get('/').data, 'GET') + self.assert_equal(c.get('/').data, b'GET') rv = c.post('/') self.assert_equal(rv.status_code, 405) self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) self.assert_(not rv.data) # head truncates - self.assert_equal(c.post('/more').data, 'POST') - self.assert_equal(c.get('/more').data, 'GET') + self.assert_equal(c.post('/more').data, b'POST') + self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') self.assert_equal(rv.status_code, 405) self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST']) @@ -124,8 +124,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.view_functions['index'] = index c = app.test_client() - self.assert_equal(c.get('/foo/').data, 'index') - self.assert_equal(c.get('/foo/bar').data, 'bar') + self.assert_equal(c.get('/foo/').data, b'index') + self.assert_equal(c.get('/foo/bar').data, b'bar') def test_endpoint_decorator(self): from werkzeug.routing import Submount, Rule @@ -144,8 +144,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'index' c = app.test_client() - self.assert_equal(c.get('/foo/').data, 'index') - self.assert_equal(c.get('/foo/bar').data, 'bar') + self.assert_equal(c.get('/foo/').data, b'index') + self.assert_equal(c.get('/foo/bar').data, b'bar') def test_session(self): app = flask.Flask(__name__) @@ -159,8 +159,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): return flask.session['value'] c = app.test_client() - self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set') - self.assert_equal(c.get('/get').data, '42') + self.assert_equal(c.post('/set', data={'value': '42'}).data, b'value set') + self.assert_equal(c.get('/get').data, b'42') def test_session_using_server_name(self): app = flask.Flask(__name__) @@ -289,7 +289,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(expires.day, expected.day) rv = client.get('/test') - self.assert_equal(rv.data, 'True') + self.assert_equal(rv.data, b'True') permanent = False rv = app.test_client().get('/') @@ -311,8 +311,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): return repr(flask.session.get('foo')) c = app.test_client() - self.assert_equal(c.get('/').data, 'None') - self.assert_equal(c.get('/').data, '42') + self.assert_equal(c.get('/').data, b'None') + self.assert_equal(c.get('/').data, b'42') def test_session_special_types(self): app = flask.Flask(__name__) @@ -454,7 +454,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_('after' not in evts) rv = app.test_client().get('/').data self.assert_('after' in evts) - self.assert_equal(rv, 'request|after') + self.assert_equal(rv, b'request|after') def test_after_request_processing(self): app = flask.Flask(__name__) @@ -483,7 +483,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_('Response' in rv.data) + self.assert_(b'Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_debug_mode(self): @@ -499,7 +499,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_('Response' in rv.data) + self.assert_(b'Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_error(self): @@ -532,7 +532,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1/0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_('Internal Server Error' in rv.data) + self.assert_(b'Internal Server Error' in rv.data) self.assert_equal(len(called), 2) def test_before_after_request_order(self): @@ -562,7 +562,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(): return '42' rv = app.test_client().get('/') - self.assert_equal(rv.data, '42') + self.assert_equal(rv.data, b'42') self.assert_equal(called, [1, 2, 3, 4, 5, 6]) def test_error_handling(self): @@ -582,10 +582,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') self.assert_equal(rv.status_code, 404) - self.assert_equal(rv.data, 'not found') + self.assert_equal(rv.data, b'not found') rv = c.get('/error') self.assert_equal(rv.status_code, 500) - self.assert_equal('internal server error', rv.data) + self.assert_equal(b'internal server error', rv.data) def test_before_request_and_routing_errors(self): app = flask.Flask(__name__) @@ -597,7 +597,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return flask.g.something, 404 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 404) - self.assert_equal(rv.data, 'value') + self.assert_equal(rv.data, b'value') def test_user_error_handling(self): class MyException(Exception): @@ -613,7 +613,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): raise MyException() c = app.test_client() - self.assert_equal(c.get('/').data, '42') + self.assert_equal(c.get('/').data, b'42') def test_trapping_of_bad_request_key_errors(self): app = flask.Flask(__name__) @@ -687,7 +687,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8')) self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8')) rv = c.get('/args') - self.assert_equal(rv.data, 'Meh') + self.assert_equal(rv.data, b'Meh') self.assert_equal(rv.headers['X-Foo'], 'Testing') self.assert_equal(rv.status_code, 400) self.assert_equal(rv.mimetype, 'text/plain') @@ -697,17 +697,17 @@ class BasicFunctionalityTestCase(FlaskTestCase): with app.test_request_context(): rv = flask.make_response() self.assert_equal(rv.status_code, 200) - self.assert_equal(rv.data, '') + self.assert_equal(rv.data, b'') self.assert_equal(rv.mimetype, 'text/html') rv = flask.make_response('Awesome') self.assert_equal(rv.status_code, 200) - self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.data, b'Awesome') self.assert_equal(rv.mimetype, 'text/html') rv = flask.make_response('W00t', 404) self.assert_equal(rv.status_code, 404) - self.assert_equal(rv.data, 'W00t') + self.assert_equal(rv.data, b'W00t') self.assert_equal(rv.mimetype, 'text/html') def test_make_response_with_response_instance(self): @@ -716,14 +716,13 @@ class BasicFunctionalityTestCase(FlaskTestCase): rv = flask.make_response( flask.jsonify({'msg': 'W00t'}), 400) self.assertEqual(rv.status_code, 400) - self.assertEqual(rv.data, - '{\n "msg": "W00t"\n}') + self.assertEqual(rv.data, b'{\n "msg": "W00t"\n}') self.assertEqual(rv.mimetype, 'application/json') rv = flask.make_response( flask.Response(''), 400) self.assertEqual(rv.status_code, 400) - self.assertEqual(rv.data, '') + self.assertEqual(rv.data, b'') self.assertEqual(rv.mimetype, 'text/html') rv = flask.make_response( @@ -783,13 +782,13 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(args): return '|'.join(args) c = app.test_client() - self.assert_equal(c.get('/1,2,3').data, '1|2|3') + self.assert_equal(c.get('/1,2,3').data, b'1|2|3') def test_static_files(self): app = flask.Flask(__name__) rv = app.test_client().get('/static/index.html') self.assert_equal(rv.status_code, 200) - self.assert_equal(rv.data.strip(), '

    Hello World!

    ') + self.assert_equal(rv.data.strip(), b'

    Hello World!

    ') with app.test_request_context(): self.assert_equal(flask.url_for('static', filename='index.html'), '/static/index.html') @@ -825,17 +824,17 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'Foo SubDomain' rv = app.test_client().get('/') - self.assert_equal(rv.data, 'Foo') + self.assert_equal(rv.data, b'Foo') rv = app.test_client().get('/', 'http://localhost.localdomain:5000') - self.assert_equal(rv.data, 'Foo') + self.assert_equal(rv.data, b'Foo') rv = app.test_client().get('/', 'https://localhost.localdomain:5000') - self.assert_equal(rv.data, 'Foo') + self.assert_equal(rv.data, b'Foo') app.config.update(SERVER_NAME='localhost.localdomain') rv = app.test_client().get('/', 'https://localhost.localdomain') - self.assert_equal(rv.data, 'Foo') + self.assert_equal(rv.data, b'Foo') try: app.config.update(SERVER_NAME='localhost.localdomain:443') @@ -860,7 +859,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): "server name from the WSGI environment ('foo.localhost')") rv = app.test_client().get('/', 'http://foo.localhost.localdomain') - self.assert_equal(rv.data, 'Foo SubDomain') + self.assert_equal(rv.data, b'Foo SubDomain') def test_exception_propagation(self): def apprunner(configkey): @@ -906,7 +905,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() rv = c.post('/accept', data={'myfile': 'foo' * 100}) - self.assert_equal(rv.data, '42') + self.assert_equal(rv.data, b'42') def test_url_processors(self): app = flask.Flask(__name__) @@ -935,9 +934,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): c = app.test_client() - self.assert_equal(c.get('/de/').data, '/de/about') - self.assert_equal(c.get('/de/about').data, '/foo') - self.assert_equal(c.get('/foo').data, '/en/about') + self.assert_equal(c.get('/de/').data, b'/de/about') + self.assert_equal(c.get('/de/about').data, b'/foo') + self.assert_equal(c.get('/foo').data, b'/en/about') def test_inject_blueprint_url_defaults(self): app = flask.Flask(__name__) @@ -969,7 +968,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def index(): return 'Awesome' self.assert_(not app.got_first_request) - self.assert_equal(app.test_client().get('/').data, 'Awesome') + self.assert_equal(app.test_client().get('/').data, b'Awesome') try: @app.route('/foo') def broken(): @@ -983,7 +982,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/foo') def working(): return 'Meh' - self.assert_equal(app.test_client().get('/foo').data, 'Meh') + self.assert_equal(app.test_client().get('/foo').data, b'Meh') self.assert_(app.got_first_request) def test_before_first_request_functions(self): @@ -1016,12 +1015,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.fail('Expected exception') rv = c.get('/foo', data={}, follow_redirects=True) - self.assert_equal(rv.data, 'success') + self.assert_equal(rv.data, b'success') app.debug = False with app.test_client() as c: rv = c.post('/foo', data={}, follow_redirects=True) - self.assert_equal(rv.data, 'success') + self.assert_equal(rv.data, b'success') def test_route_decorator_custom_endpoint(self): app = flask.Flask(__name__) @@ -1045,9 +1044,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): assert flask.url_for('123') == '/bar/123' c = app.test_client() - self.assertEqual(c.get('/foo/').data, 'foo') - self.assertEqual(c.get('/bar/').data, 'bar') - self.assertEqual(c.get('/bar/123').data, '123') + self.assertEqual(c.get('/foo/').data, b'foo') + self.assertEqual(c.get('/bar/').data, b'bar') + self.assertEqual(c.get('/bar/123').data, b'123') def test_preserve_only_once(self): app = flask.Flask(__name__) @@ -1084,10 +1083,10 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/', 'http://localhost/') - self.assert_equal(rv.data, 'normal index') + self.assert_equal(rv.data, b'normal index') rv = c.get('/', 'http://test.localhost/') - self.assert_equal(rv.data, 'test index') + self.assert_equal(rv.data, b'test index') @emits_module_deprecation_warning def test_module_static_path_subdomain(self): @@ -1108,7 +1107,7 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/', 'http://mitsuhiko.localhost/') - self.assert_equal(rv.data, 'index for mitsuhiko') + self.assert_equal(rv.data, b'index for mitsuhiko') def test_subdomain_matching_with_ports(self): app = flask.Flask(__name__) @@ -1119,7 +1118,7 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/', 'http://mitsuhiko.localhost:3000/') - self.assert_equal(rv.data, 'index for mitsuhiko') + self.assert_equal(rv.data, b'index for mitsuhiko') @emits_module_deprecation_warning def test_module_subdomain_support(self): @@ -1139,9 +1138,9 @@ class SubdomainTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/test', 'http://testing.localhost/') - self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.data, b'Test') rv = c.get('/outside', 'http://xtesting.localhost/') - self.assert_equal(rv.data, 'Outside') + self.assert_equal(rv.data, b'Outside') def suite(): diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 331854a6..28256b63 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -46,10 +46,10 @@ class ModuleTestCase(FlaskTestCase): return 'the index' app.register_module(admin) c = app.test_client() - self.assert_equal(c.get('/').data, 'the index') - self.assert_equal(c.get('/admin/').data, 'admin index') - self.assert_equal(c.get('/admin/login').data, 'admin login') - self.assert_equal(c.get('/admin/logout').data, 'admin logout') + self.assert_equal(c.get('/').data, b'the index') + self.assert_equal(c.get('/admin/').data, b'admin index') + self.assert_equal(c.get('/admin/login').data, b'admin login') + self.assert_equal(c.get('/admin/logout').data, b'admin logout') @emits_module_deprecation_warning def test_default_endpoint_name(self): @@ -60,7 +60,7 @@ class ModuleTestCase(FlaskTestCase): mod.add_url_rule('/', view_func=index) app.register_module(mod) rv = app.test_client().get('/') - self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.data, b'Awesome') with app.test_request_context(): self.assert_equal(flask.url_for('frontend.index'), '/') @@ -92,11 +92,11 @@ class ModuleTestCase(FlaskTestCase): app.register_module(admin) c = app.test_client() - self.assert_equal(c.get('/').data, 'the index') + self.assert_equal(c.get('/').data, b'the index') self.assert_equal(catched, ['before-app', 'after-app']) del catched[:] - self.assert_equal(c.get('/admin/').data, 'the admin') + self.assert_equal(c.get('/admin/').data, b'the admin') self.assert_equal(catched, ['before-app', 'before-admin', 'after-admin', 'after-app']) @@ -121,8 +121,8 @@ class ModuleTestCase(FlaskTestCase): return flask.render_template_string('{{ a }}{{ b }}{{ c }}') app.register_module(admin) c = app.test_client() - self.assert_equal(c.get('/').data, '13') - self.assert_equal(c.get('/admin/').data, '123') + self.assert_equal(c.get('/').data, b'13') + self.assert_equal(c.get('/admin/').data, b'123') @emits_module_deprecation_warning def test_late_binding(self): @@ -132,7 +132,7 @@ class ModuleTestCase(FlaskTestCase): def index(): return '42' app.register_module(admin, url_prefix='/admin') - self.assert_equal(app.test_client().get('/admin/').data, '42') + self.assert_equal(app.test_client().get('/admin/').data, b'42') @emits_module_deprecation_warning def test_error_handling(self): @@ -154,7 +154,7 @@ class ModuleTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') self.assert_equal(rv.status_code, 404) - self.assert_equal(rv.data, 'not found') + self.assert_equal(rv.data, b'not found') rv = c.get('/error') self.assert_equal(rv.status_code, 500) self.assert_equal('internal server error', rv.data) @@ -165,11 +165,11 @@ class ModuleTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') - self.assert_equal(rv.data, 'Hello from the Frontend') + self.assert_equal(rv.data, b'Hello from the Frontend') rv = c.get('/admin/') - self.assert_equal(rv.data, 'Hello from the Admin') + self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/index2') - self.assert_equal(rv.data, 'Hello from the Admin') + self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') self.assert_equal(rv.data.strip(), 'Admin File') rv = c.get('/admin/static/css/test.css') @@ -248,8 +248,8 @@ class ModuleTestCase(FlaskTestCase): app.register_module(module) c = app.test_client() - self.assert_equal(c.get('/foo/').data, 'index') - self.assert_equal(c.get('/foo/bar').data, 'bar') + self.assert_equal(c.get('/foo/').data, b'index') + self.assert_equal(c.get('/foo/bar').data, b'bar') class BlueprintTestCase(FlaskTestCase): @@ -290,9 +290,9 @@ class BlueprintTestCase(FlaskTestCase): c = app.test_client() - self.assert_equal(c.get('/frontend-no').data, 'frontend says no') - self.assert_equal(c.get('/backend-no').data, 'backend says no') - self.assert_equal(c.get('/what-is-a-sideend').data, 'application itself says no') + self.assert_equal(c.get('/frontend-no').data, b'frontend says no') + self.assert_equal(c.get('/backend-no').data, b'backend says no') + self.assert_equal(c.get('/what-is-a-sideend').data, b'application itself says no') def test_blueprint_url_definitions(self): bp = flask.Blueprint('test', __name__) @@ -339,19 +339,19 @@ class BlueprintTestCase(FlaskTestCase): c = app.test_client() - self.assert_equal(c.get('/de/').data, '/de/about') - self.assert_equal(c.get('/de/about').data, '/de/') + self.assert_equal(c.get('/de/').data, b'/de/about') + self.assert_equal(c.get('/de/about').data, b'/de/') def test_templates_and_static(self): from blueprintapp import app c = app.test_client() rv = c.get('/') - self.assert_equal(rv.data, 'Hello from the Frontend') + self.assert_equal(rv.data, b'Hello from the Frontend') rv = c.get('/admin/') - self.assert_equal(rv.data, 'Hello from the Admin') + self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/index2') - self.assert_equal(rv.data, 'Hello from the Admin') + self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') self.assert_equal(rv.data.strip(), 'Admin File') rv = c.get('/admin/static/css/test.css') @@ -451,8 +451,8 @@ class BlueprintTestCase(FlaskTestCase): app.register_blueprint(bp) c = app.test_client() - self.assert_equal(c.get('/').data, '1') - self.assert_equal(c.get('/page/2').data, '2') + self.assert_equal(c.get('/').data, b'1') + self.assert_equal(c.get('/page/2').data, b'2') def test_route_decorator_custom_endpoint(self): @@ -482,11 +482,11 @@ class BlueprintTestCase(FlaskTestCase): return flask.request.endpoint c = app.test_client() - self.assertEqual(c.get('/').data, 'index') - self.assertEqual(c.get('/py/foo').data, 'bp.foo') - self.assertEqual(c.get('/py/bar').data, 'bp.bar') - self.assertEqual(c.get('/py/bar/123').data, 'bp.123') - self.assertEqual(c.get('/py/bar/foo').data, 'bp.bar_foo') + self.assertEqual(c.get('/').data, b'index') + self.assertEqual(c.get('/py/foo').data, b'bp.foo') + self.assertEqual(c.get('/py/bar').data, b'bp.bar') + self.assertEqual(c.get('/py/bar/123').data, b'bp.123') + self.assertEqual(c.get('/py/bar/foo').data, b'bp.bar_foo') def test_route_decorator_custom_endpoint_with_dots(self): bp = flask.Blueprint('bp', __name__) @@ -533,7 +533,7 @@ class BlueprintTestCase(FlaskTestCase): app.register_blueprint(bp, url_prefix='/py') c = app.test_client() - self.assertEqual(c.get('/py/foo').data, 'bp.foo') + self.assertEqual(c.get('/py/foo').data, b'bp.foo') # The rule's din't actually made it through rv = c.get('/py/bar') assert rv.status_code == 404 @@ -595,7 +595,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_template_filter_after_route_with_template(self): app = flask.Flask(__name__) @@ -608,7 +608,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app.register_blueprint(bp, url_prefix='/py') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_add_template_filter_with_template(self): bp = flask.Blueprint('bp', __name__) @@ -621,7 +621,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_template_filter_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -634,7 +634,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_add_template_filter_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -647,7 +647,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_template_test(self): bp = flask.Blueprint('bp', __name__) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 2808a409..fbdcbff4 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -99,7 +99,7 @@ class JSONTestCase(FlaskTestCase): c = app.test_client() rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') - self.assert_equal(rv.data, '3') + self.assert_equal(rv.data, b'3') def test_template_escaping(self): app = flask.Flask(__name__) @@ -140,7 +140,7 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/', data=flask.json.dumps({ 'x': {'_foo': 42} }), content_type='application/json') - self.assertEqual(rv.data, '"<42>"') + self.assertEqual(rv.data, b'"<42>"') def test_modified_url_encoding(self): class ModifiedRequest(flask.Request): @@ -212,14 +212,14 @@ class SendfileTestCase(FlaskTestCase): with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f) - self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'application/octet-stream') # etags self.assert_equal(len(captured), 1) with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f, mimetype='text/plain') - self.assert_equal(rv.data, 'Test') + self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'text/plain') # etags self.assert_equal(len(captured), 1) @@ -385,7 +385,7 @@ class LoggingTestCase(FlaskTestCase): for trigger in 'before', 'after': rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_equal(rv.data, 'Hello Server Error') + self.assert_equal(rv.data, b'Hello Server Error') def test_url_for_with_anchor(self): app = flask.Flask(__name__) @@ -477,7 +477,7 @@ class StreamingTestCase(FlaskTestCase): return flask.Response(flask.stream_with_context(generate())) c = app.test_client() rv = c.get('/?name=World') - self.assertEqual(rv.data, 'Hello World!') + self.assertEqual(rv.data, b'Hello World!') def test_streaming_with_context_as_decorator(self): app = flask.Flask(__name__) @@ -492,7 +492,7 @@ class StreamingTestCase(FlaskTestCase): return flask.Response(generate()) c = app.test_client() rv = c.get('/?name=World') - self.assertEqual(rv.data, 'Hello World!') + self.assertEqual(rv.data, b'Hello World!') def test_streaming_with_context_and_custom_close(self): app = flask.Flask(__name__) @@ -517,7 +517,7 @@ class StreamingTestCase(FlaskTestCase): Wrapper(generate()))) c = app.test_client() rv = c.get('/?name=World') - self.assertEqual(rv.data, 'Hello World!') + self.assertEqual(rv.data, b'Hello World!') self.assertEqual(called, [42]) diff --git a/flask/testsuite/regression.py b/flask/testsuite/regression.py index b8140d6f..e516dc0f 100644 --- a/flask/testsuite/regression.py +++ b/flask/testsuite/regression.py @@ -66,7 +66,7 @@ class MemoryTestCase(FlaskTestCase): with app.test_client() as c: rv = c.get('/') self.assert_equal(rv.status_code, 200) - self.assert_equal(rv.data, '

    42

    ') + self.assert_equal(rv.data, b'

    42

    ') # Trigger caches fire() @@ -105,7 +105,7 @@ class ExceptionTestCase(FlaskTestCase): rv = c.get('/') self.assertEqual(rv.headers['Location'], 'http://localhost/test') rv = c.get('/test') - self.assertEqual(rv.data, '42') + self.assertEqual(rv.data, b'42') def suite(): diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py index 0f0282e5..6e379448 100644 --- a/flask/testsuite/reqctx.py +++ b/flask/testsuite/reqctx.py @@ -145,7 +145,7 @@ class RequestContextTestCase(FlaskTestCase): return 'Hello World!' rv = app.test_client().get('/?foo=bar') - self.assert_equal(rv.data, 'Hello World!') + self.assert_equal(rv.data, b'Hello World!') result = greenlets[0].run() self.assert_equal(result, 42) @@ -168,7 +168,7 @@ class RequestContextTestCase(FlaskTestCase): return 'Hello World!' rv = app.test_client().get('/?foo=bar') - self.assert_equal(rv.data, 'Hello World!') + self.assert_equal(rv.data, b'Hello World!') result = greenlets[0].run() self.assert_equal(result, 42) diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index 637c2c66..843c2f83 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -45,7 +45,7 @@ class SignalsTestCase(FlaskTestCase): calls.append('before-signal') def after_request_signal(sender, response): - self.assert_equal(response.data, 'stuff') + self.assert_equal(response.data, b'stuff') calls.append('after-signal') @app.before_request @@ -68,7 +68,7 @@ class SignalsTestCase(FlaskTestCase): try: rv = app.test_client().get('/') - self.assert_equal(rv.data, 'stuff') + self.assert_equal(rv.data, b'stuff') self.assert_equal(calls, ['before-signal', 'before-handler', 'handler', 'after-handler', diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index c8d1dd22..c36a282e 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -25,7 +25,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('context_template.html', value=23) rv = app.test_client().get('/') - self.assert_equal(rv.data, '

    23|42') + self.assert_equal(rv.data, b'

    23|42') def test_original_win(self): app = flask.Flask(__name__) @@ -33,7 +33,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template_string('{{ config }}', config=42) rv = app.test_client().get('/') - self.assert_equal(rv.data, '42') + self.assert_equal(rv.data, b'42') def test_request_less_rendering(self): app = flask.Flask(__name__) @@ -139,7 +139,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_add_template_filter_with_template(self): app = flask.Flask(__name__) @@ -150,7 +150,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_template_filter_with_name_and_template(self): app = flask.Flask(__name__) @@ -161,7 +161,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_add_template_filter_with_name_and_template(self): app = flask.Flask(__name__) @@ -172,7 +172,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_filter.html', value='abcd') rv = app.test_client().get('/') - self.assert_equal(rv.data, 'dcba') + self.assert_equal(rv.data, b'dcba') def test_template_test(self): app = flask.Flask(__name__) @@ -277,7 +277,7 @@ class TemplatingTestCase(FlaskTestCase): return flask.render_template('index.html') c = app.test_client() rv = c.get('/') - self.assert_equal(rv.data, 'Hello Custom World!') + self.assert_equal(rv.data, b'Hello Custom World!') def test_iterable_loader(self): app = flask.Flask(__name__) @@ -293,7 +293,7 @@ class TemplatingTestCase(FlaskTestCase): value=23) rv = app.test_client().get('/') - self.assert_equal(rv.data, '

    Jameson

    ') + self.assert_equal(rv.data, b'

    Jameson

    ') def suite(): diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index a37f6e6c..8b1b7d62 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -30,7 +30,7 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(ctx.request.url, 'http://example.com:1234/foo/') with app.test_client() as c: rv = c.get('/') - self.assert_equal(rv.data, 'http://example.com:1234/foo/') + self.assert_equal(rv.data, b'http://example.com:1234/foo/') def test_environ_defaults(self): app = flask.Flask(__name__) @@ -43,7 +43,7 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(ctx.request.url, 'http://localhost/') with app.test_client() as c: rv = c.get('/') - self.assert_equal(rv.data, 'http://localhost/') + self.assert_equal(rv.data, b'http://localhost/') def test_redirect_keep_session(self): app = flask.Flask(__name__) @@ -92,7 +92,7 @@ class TestToolsTestCase(FlaskTestCase): sess['foo'] = [42] self.assert_equal(len(sess), 1) rv = c.get('/') - self.assert_equal(rv.data, '[42]') + self.assert_equal(rv.data, b'[42]') with c.session_transaction() as sess: self.assert_equal(len(sess), 1) self.assert_equal(sess['foo'], [42]) @@ -148,7 +148,7 @@ class TestToolsTestCase(FlaskTestCase): with app.test_client() as c: resp = c.get('/') self.assert_equal(flask.g.value, 42) - self.assert_equal(resp.data, 'Hello World!') + self.assert_equal(resp.data, b'Hello World!') self.assert_equal(resp.status_code, 200) resp = c.get('/other') diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index 6c3ae816..9dd463f2 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -20,8 +20,8 @@ class ViewTestCase(FlaskTestCase): def common_test(self, app): c = app.test_client() - self.assert_equal(c.get('/').data, 'GET') - self.assert_equal(c.post('/').data, 'POST') + self.assert_equal(c.get('/').data, b'GET') + self.assert_equal(c.post('/').data, b'POST') self.assert_equal(c.put('/').status_code, 405) meths = parse_set_header(c.open('/', method='OPTIONS').headers['Allow']) self.assert_equal(sorted(meths), ['GET', 'HEAD', 'OPTIONS', 'POST']) @@ -108,7 +108,7 @@ class ViewTestCase(FlaskTestCase): c = app.test_client() rv = c.get('/') self.assert_equal(rv.headers['X-Parachute'], 'awesome') - self.assert_equal(rv.data, 'Awesome') + self.assert_equal(rv.data, b'Awesome') def test_implicit_head(self): app = flask.Flask(__name__) @@ -122,10 +122,10 @@ class ViewTestCase(FlaskTestCase): app.add_url_rule('/', view_func=Index.as_view('index')) c = app.test_client() rv = c.get('/') - self.assert_equal(rv.data, 'Blub') + self.assert_equal(rv.data, b'Blub') self.assert_equal(rv.headers['X-Method'], 'GET') rv = c.head('/') - self.assert_equal(rv.data, '') + self.assert_equal(rv.data, b'') self.assert_equal(rv.headers['X-Method'], 'HEAD') def test_explicit_head(self): @@ -140,9 +140,9 @@ class ViewTestCase(FlaskTestCase): app.add_url_rule('/', view_func=Index.as_view('index')) c = app.test_client() rv = c.get('/') - self.assert_equal(rv.data, 'GET') + self.assert_equal(rv.data, b'GET') rv = c.head('/') - self.assert_equal(rv.data, '') + self.assert_equal(rv.data, b'') self.assert_equal(rv.headers['X-Method'], 'HEAD') def test_endpoint_override(self): From 5b89355b1c9771431fb8a011094591ad43461b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 17:14:07 +0200 Subject: [PATCH 1218/3747] Response data is bytes --- flask/testsuite/basic.py | 8 ++++---- flask/testsuite/blueprints.py | 34 +++++++++++++++++----------------- flask/testsuite/helpers.py | 2 +- flask/testsuite/subclassing.py | 2 +- flask/testsuite/templating.py | 22 +++++++++++----------- flask/testsuite/testing.py | 14 +++++++------- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 1636bee7..c76204d7 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -453,7 +453,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'request' self.assert_('after' not in evts) rv = app.test_client().get('/').data - self.assert_('after' in evts) + self.assert_(b'after' in evts) self.assert_equal(rv, b'request|after') def test_after_request_processing(self): @@ -937,10 +937,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(c.get('/de/').data, b'/de/about') self.assert_equal(c.get('/de/about').data, b'/foo') self.assert_equal(c.get('/foo').data, b'/en/about') - + def test_inject_blueprint_url_defaults(self): app = flask.Flask(__name__) - bp = flask.Blueprint('foo.bar.baz', __name__, + bp = flask.Blueprint('foo.bar.baz', __name__, template_folder='template') @bp.url_defaults @@ -1096,7 +1096,7 @@ class SubdomainTestCase(FlaskTestCase): app.register_module(mod) c = app.test_client() rv = c.get('/static/hello.txt', 'http://foo.example.com/') - self.assert_equal(rv.data.strip(), 'Hello Subdomain') + self.assert_equal(rv.data.strip(), b'Hello Subdomain') def test_subdomain_matching(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 28256b63..ed3ddbb2 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -157,7 +157,7 @@ class ModuleTestCase(FlaskTestCase): self.assert_equal(rv.data, b'not found') rv = c.get('/error') self.assert_equal(rv.status_code, 500) - self.assert_equal('internal server error', rv.data) + self.assert_equal(b'internal server error', rv.data) def test_templates_and_static(self): app = moduleapp @@ -171,9 +171,9 @@ class ModuleTestCase(FlaskTestCase): rv = c.get('/admin/index2') self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') - self.assert_equal(rv.data.strip(), 'Admin File') + self.assert_equal(rv.data.strip(), b'Admin File') rv = c.get('/admin/static/css/test.css') - self.assert_equal(rv.data.strip(), '/* nested file */') + self.assert_equal(rv.data.strip(), b'/* nested file */') with app.test_request_context(): self.assert_equal(flask.url_for('admin.static', filename='test.txt'), @@ -310,10 +310,10 @@ class BlueprintTestCase(FlaskTestCase): app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) c = app.test_client() - self.assert_equal(c.get('/1/foo').data, u'23/42') - self.assert_equal(c.get('/2/foo').data, u'19/42') - self.assert_equal(c.get('/1/bar').data, u'23') - self.assert_equal(c.get('/2/bar').data, u'19') + self.assert_equal(c.get('/1/foo').data, b'23/42') + self.assert_equal(c.get('/2/foo').data, b'19/42') + self.assert_equal(c.get('/1/bar').data, b'23') + self.assert_equal(c.get('/2/bar').data, b'19') def test_blueprint_url_processors(self): bp = flask.Blueprint('frontend', __name__, url_prefix='/') @@ -353,9 +353,9 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/admin/index2') self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') - self.assert_equal(rv.data.strip(), 'Admin File') + self.assert_equal(rv.data.strip(), b'Admin File') rv = c.get('/admin/static/css/test.css') - self.assert_equal(rv.data.strip(), '/* nested file */') + self.assert_equal(rv.data.strip(), b'/* nested file */') # try/finally, in case other tests use this app for Blueprint tests. max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT'] @@ -435,9 +435,9 @@ class BlueprintTestCase(FlaskTestCase): app.register_blueprint(backend) c = app.test_client() - self.assert_equal(c.get('/fe').data.strip(), '/be') - self.assert_equal(c.get('/fe2').data.strip(), '/fe') - self.assert_equal(c.get('/be').data.strip(), '/fe') + self.assert_equal(c.get('/fe').data.strip(), b'/be') + self.assert_equal(c.get('/fe2').data.strip(), b'/fe') + self.assert_equal(c.get('/be').data.strip(), b'/fe') def test_empty_url_defaults(self): bp = flask.Blueprint('bp', __name__) @@ -704,7 +704,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_template_test_after_route_with_template(self): app = flask.Flask(__name__) @@ -717,7 +717,7 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app.register_blueprint(bp, url_prefix='/py') rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_add_template_test_with_template(self): bp = flask.Blueprint('bp', __name__) @@ -730,7 +730,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -743,7 +743,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_add_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -756,7 +756,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index fbdcbff4..9ca9eec3 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -357,7 +357,7 @@ class LoggingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_('Internal Server Error' in rv.data) + self.assert_(b'Internal Server Error' in rv.data) err = out.getvalue() self.assert_('Exception on / [GET]' in err) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index 3e8d75c1..e26b9085 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -34,7 +34,7 @@ class FlaskSubclassingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_('Internal Server Error' in rv.data) + self.assert_(b'Internal Server Error' in rv.data) err = out.getvalue() self.assert_equal(err, '') diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index c36a282e..0d3a2a5c 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -61,7 +61,7 @@ class TemplatingTestCase(FlaskTestCase): {{ session.test }} ''') rv = app.test_client().get('/?foo=42') - self.assert_equal(rv.data.split(), ['42', '23', 'False', 'aha']) + self.assert_equal(rv.data.split(), [b'42', b'23', b'False', b'aha']) def test_escaping(self): text = '

    Hello World!' @@ -72,12 +72,12 @@ class TemplatingTestCase(FlaskTestCase): html=flask.Markup(text)) lines = app.test_client().get('/').data.splitlines() self.assert_equal(lines, [ - '<p>Hello World!', - '

    Hello World!', - '

    Hello World!', - '

    Hello World!', - '<p>Hello World!', - '

    Hello World!' + b'<p>Hello World!', + b'

    Hello World!', + b'

    Hello World!', + b'

    Hello World!', + b'<p>Hello World!', + b'

    Hello World!' ]) def test_no_escaping(self): @@ -219,7 +219,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_add_template_test_with_template(self): app = flask.Flask(__name__) @@ -230,7 +230,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -241,7 +241,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_add_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -252,7 +252,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_('Success!' in rv.data) + self.assert_(b'Success!' in rv.data) def test_add_template_global(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 8b1b7d62..3379a463 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -62,20 +62,20 @@ class TestToolsTestCase(FlaskTestCase): with app.test_client() as c: rv = c.get('/getsession') - assert rv.data == '' + assert rv.data == b'' rv = c.get('/') - assert rv.data == 'index' + assert rv.data == b'index' assert flask.session.get('data') == 'foo' rv = c.post('/', data={}, follow_redirects=True) - assert rv.data == 'foo' + assert rv.data == b'foo' # This support requires a new Werkzeug version if not hasattr(c, 'redirect_client'): assert flask.session.get('data') == 'foo' rv = c.get('/getsession') - assert rv.data == 'foo' + assert rv.data == b'foo' def test_session_transactions(self): app = flask.Flask(__name__) @@ -153,7 +153,7 @@ class TestToolsTestCase(FlaskTestCase): resp = c.get('/other') self.assert_(not hasattr(flask.g, 'value')) - self.assert_('Internal Server Error' in resp.data) + self.assert_(b'Internal Server Error' in resp.data) self.assert_equal(resp.status_code, 500) flask.g.value = 23 @@ -220,7 +220,7 @@ class SubdomainTestCase(FlaskTestCase): response = self.client.get(url) self.assertEquals(200, response.status_code) - self.assertEquals('xxx', response.data) + self.assertEquals(b'xxx', response.data) def test_nosubdomain(self): @@ -232,7 +232,7 @@ class SubdomainTestCase(FlaskTestCase): response = self.client.get(url) self.assertEquals(200, response.status_code) - self.assertEquals('xxx', response.data) + self.assertEquals(b'xxx', response.data) def suite(): From 239780be289efd365c07be2f60bab9223388399e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 17:23:38 +0200 Subject: [PATCH 1219/3747] Use assert_true instead of assert_ assert_ is deprecated which causes annoying warnings --- flask/testsuite/__init__.py | 7 ++- flask/testsuite/appctx.py | 2 +- flask/testsuite/basic.py | 92 +++++++++++++++++----------------- flask/testsuite/blueprints.py | 44 ++++++++-------- flask/testsuite/config.py | 24 ++++----- flask/testsuite/ext.py | 8 +-- flask/testsuite/helpers.py | 38 +++++++------- flask/testsuite/reqctx.py | 24 ++++----- flask/testsuite/signals.py | 2 +- flask/testsuite/subclassing.py | 2 +- flask/testsuite/templating.py | 36 ++++++------- flask/testsuite/testing.py | 12 ++--- 12 files changed, 147 insertions(+), 144 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 82fa3232..2a869173 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -101,9 +101,9 @@ def emits_module_deprecation_warning(f): def new_f(self, *args, **kwargs): with catch_warnings() as log: f(self, *args, **kwargs) - self.assert_(log, 'expected deprecation warning') + self.assert_true(log, 'expected deprecation warning') for entry in log: - self.assert_('Modules are deprecated' in str(entry['message'])) + self.assert_true('Modules are deprecated' in str(entry['message'])) return update_wrapper(new_f, f) @@ -142,6 +142,9 @@ class FlaskTestCase(unittest.TestCase): with catcher: callable(*args, **kwargs) + def assert_true(self, x): + self.assertTrue(x) + class _ExceptionCatcher(object): diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index afed923a..54b9014e 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -87,7 +87,7 @@ class AppContextTestCase(FlaskTestCase): with flask._app_ctx_stack.top: with flask._request_ctx_stack.top: pass - self.assert_(flask._request_ctx_stack.request.environ + self.assert_true(flask._request_ctx_stack.request.environ ['werkzeug.request'] is not None) c = app.test_client() c.get('/') diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index c76204d7..20c2f0cc 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -78,7 +78,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - self.assert_(not rv.data) # head truncates + self.assert_true(not rv.data) # head truncates self.assert_equal(c.post('/more').data, b'POST') self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') @@ -102,7 +102,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - self.assert_(not rv.data) # head truncates + self.assert_true(not rv.data) # head truncates self.assert_equal(c.post('/more').data, b'POST') self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') @@ -173,8 +173,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com/') - self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) - self.assert_('httponly' in rv.headers['set-cookie'].lower()) + self.assert_true('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_true('httponly' in rv.headers['set-cookie'].lower()) def test_session_using_server_name_and_port(self): app = flask.Flask(__name__) @@ -187,8 +187,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower()) - self.assert_('httponly' in rv.headers['set-cookie'].lower()) + self.assert_true('domain=.example.com' in rv.headers['set-cookie'].lower()) + self.assert_true('httponly' in rv.headers['set-cookie'].lower()) def test_session_using_server_name_port_and_path(self): app = flask.Flask(__name__) @@ -202,9 +202,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/foo') - self.assert_('domain=example.com' in rv.headers['set-cookie'].lower()) - self.assert_('path=/foo' in rv.headers['set-cookie'].lower()) - self.assert_('httponly' in rv.headers['set-cookie'].lower()) + self.assert_true('domain=example.com' in rv.headers['set-cookie'].lower()) + self.assert_true('path=/foo' in rv.headers['set-cookie'].lower()) + self.assert_true('httponly' in rv.headers['set-cookie'].lower()) def test_session_using_application_root(self): class PrefixPathMiddleware(object): @@ -226,7 +226,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - self.assert_('path=/bar' in rv.headers['set-cookie'].lower()) + self.assert_true('path=/bar' in rv.headers['set-cookie'].lower()) def test_session_using_session_settings(self): app = flask.Flask(__name__) @@ -245,10 +245,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'Hello World' rv = app.test_client().get('/', 'http://www.example.com:8080/test/') cookie = rv.headers['set-cookie'].lower() - self.assert_('domain=.example.com' in cookie) - self.assert_('path=/;' in cookie) - self.assert_('secure' in cookie) - self.assert_('httponly' not in cookie) + self.assert_true('domain=.example.com' in cookie) + self.assert_true('path=/;' in cookie) + self.assert_true('secure' in cookie) + self.assert_true('httponly' not in cookie) def test_missing_session(self): app = flask.Flask(__name__) @@ -256,11 +256,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: f(*args, **kwargs) except RuntimeError as e: - self.assert_(e.args and 'session is unavailable' in e.args[0]) + self.assert_true(e.args and 'session is unavailable' in e.args[0]) else: - self.assert_(False, 'expected exception') + self.assert_true(False, 'expected exception') with app.test_request_context(): - self.assert_(flask.session.get('missing_key') is None) + self.assert_true(flask.session.get('missing_key') is None) expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') @@ -280,7 +280,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): client = app.test_client() rv = client.get('/') - self.assert_('set-cookie' in rv.headers) + self.assert_true('set-cookie' in rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime @@ -293,9 +293,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): permanent = False rv = app.test_client().get('/') - self.assert_('set-cookie' in rv.headers) + self.assert_true('set-cookie' in rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) - self.assert_(match is None) + self.assert_true(match is None) def test_session_stored_last(self): app = flask.Flask(__name__) @@ -347,11 +347,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.secret_key = 'testkey' with app.test_request_context(): - self.assert_(not flask.session.modified) + self.assert_true(not flask.session.modified) flask.flash('Zap') flask.session.modified = False flask.flash('Zip') - self.assert_(flask.session.modified) + self.assert_true(flask.session.modified) self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip']) def test_extended_flashing(self): @@ -448,12 +448,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): return response @app.route('/') def index(): - self.assert_('before' in evts) - self.assert_('after' not in evts) + self.assert_true('before' in evts) + self.assert_true('after' not in evts) return 'request' - self.assert_('after' not in evts) + self.assert_true('after' not in evts) rv = app.test_client().get('/').data - self.assert_(b'after' in evts) + self.assert_true(b'after' in evts) self.assert_equal(rv, b'request|after') def test_after_request_processing(self): @@ -483,7 +483,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_(b'Response' in rv.data) + self.assert_true(b'Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_debug_mode(self): @@ -499,7 +499,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_(b'Response' in rv.data) + self.assert_true(b'Response' in rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_error(self): @@ -532,7 +532,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1/0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_(b'Internal Server Error' in rv.data) + self.assert_true(b'Internal Server Error' in rv.data) self.assert_equal(len(called), 2) def test_before_after_request_order(self): @@ -606,7 +606,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.errorhandler(MyException) def handle_my_exception(e): - self.assert_(isinstance(e, MyException)) + self.assert_true(isinstance(e, MyException)) return '42' @app.route('/') def index(): @@ -629,7 +629,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.get('/fail') except KeyError as e: - self.assert_(isinstance(e, BadRequest)) + self.assert_true(isinstance(e, BadRequest)) else: self.fail('Expected exception') @@ -664,8 +664,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.post('/fail', data={'foo': 'index.txt'}) except DebugFilesKeyError as e: - self.assert_('no file contents were transmitted' in str(e)) - self.assert_('This was submitted: "index.txt"' in str(e)) + self.assert_true('no file contents were transmitted' in str(e)) + self.assert_true('This was submitted: "index.txt"' in str(e)) else: self.fail('Expected exception') @@ -804,7 +804,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(str(e), 'View function did not return a response') pass else: - self.assert_("Expected ValueError") + self.assert_true("Expected ValueError") def test_request_locals(self): self.assert_equal(repr(flask.g), '') @@ -894,11 +894,11 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.before_request def always_first(): flask.request.form['myfile'] - self.assert_(False) + self.assert_true(False) @app.route('/accept', methods=['POST']) def accept_file(): flask.request.form['myfile'] - self.assert_(False) + self.assert_true(False) @app.errorhandler(413) def catcher(error): return '42' @@ -967,14 +967,14 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/') def index(): return 'Awesome' - self.assert_(not app.got_first_request) + self.assert_true(not app.got_first_request) self.assert_equal(app.test_client().get('/').data, b'Awesome') try: @app.route('/foo') def broken(): return 'Meh' except AssertionError as e: - self.assert_('A setup function was called' in str(e)) + self.assert_true('A setup function was called' in str(e)) else: self.fail('Expected exception') @@ -983,7 +983,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def working(): return 'Meh' self.assert_equal(app.test_client().get('/foo').data, b'Meh') - self.assert_(app.got_first_request) + self.assert_true(app.got_first_request) def test_before_first_request_functions(self): got = [] @@ -996,7 +996,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(got, [42]) c.get('/') self.assert_equal(got, [42]) - self.assert_(app.got_first_request) + self.assert_true(app.got_first_request) def test_routing_redirect_debugging(self): app = flask.Flask(__name__) @@ -1008,8 +1008,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.post('/foo', data={}) except AssertionError as e: - self.assert_('http://localhost/foo/' in str(e)) - self.assert_('Make sure to directly send your POST-request ' + self.assert_true('http://localhost/foo/' in str(e)) + self.assert_true('Make sure to directly send your POST-request ' 'to this URL' in str(e)) else: self.fail('Expected exception') @@ -1061,12 +1061,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): with self.assert_raises(ZeroDivisionError): c.get('/fail') - self.assert_(flask._request_ctx_stack.top is not None) - self.assert_(flask._app_ctx_stack.top is not None) + self.assert_true(flask._request_ctx_stack.top is not None) + self.assert_true(flask._app_ctx_stack.top is not None) # implicit appctx disappears too flask._request_ctx_stack.top.pop() - self.assert_(flask._request_ctx_stack.top is None) - self.assert_(flask._app_ctx_stack.top is None) + self.assert_true(flask._request_ctx_stack.top is None) + self.assert_true(flask._app_ctx_stack.top is None) class SubdomainTestCase(FlaskTestCase): diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index ed3ddbb2..088c72d2 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -185,7 +185,7 @@ class ModuleTestCase(FlaskTestCase): except TemplateNotFound as e: self.assert_equal(e.name, 'missing.html') else: - self.assert_(0, 'expected exception') + self.assert_true(0, 'expected exception') with flask.Flask(__name__).test_request_context(): self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') @@ -201,13 +201,13 @@ class ModuleTestCase(FlaskTestCase): except NotFound: pass else: - self.assert_(0, 'expected exception') + self.assert_true(0, 'expected exception') try: f('../__init__.py') except NotFound: pass else: - self.assert_(0, 'expected exception') + self.assert_true(0, 'expected exception') # testcase for a security issue that may exist on windows systems import os @@ -220,7 +220,7 @@ class ModuleTestCase(FlaskTestCase): except NotFound: pass else: - self.assert_(0, 'expected exception') + self.assert_true(0, 'expected exception') finally: os.path = old_path @@ -380,7 +380,7 @@ class BlueprintTestCase(FlaskTestCase): except TemplateNotFound as e: self.assert_equal(e.name, 'missing.html') else: - self.assert_(0, 'expected exception') + self.assert_true(0, 'expected exception') with flask.Flask(__name__).test_request_context(): self.assert_equal(flask.render_template('nested/nested.txt'), 'I\'m nested') @@ -547,7 +547,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_true('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -558,7 +558,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_true('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -569,7 +569,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_true('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -580,7 +580,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse, 'strrev') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_true('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -656,9 +656,9 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_true('is_boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) - self.assert_(app.jinja_env.tests['is_boolean'](False)) + self.assert_true(app.jinja_env.tests['is_boolean'](False)) def test_add_template_test(self): bp = flask.Blueprint('bp', __name__) @@ -667,9 +667,9 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_test(is_boolean) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_true('is_boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) - self.assert_(app.jinja_env.tests['is_boolean'](False)) + self.assert_true(app.jinja_env.tests['is_boolean'](False)) def test_template_test_with_name(self): bp = flask.Blueprint('bp', __name__) @@ -678,9 +678,9 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_add_template_test_with_name(self): bp = flask.Blueprint('bp', __name__) @@ -689,9 +689,9 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_test(is_boolean, 'boolean') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_template_test_with_template(self): bp = flask.Blueprint('bp', __name__) @@ -704,7 +704,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_template_test_after_route_with_template(self): app = flask.Flask(__name__) @@ -717,7 +717,7 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app.register_blueprint(bp, url_prefix='/py') rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_add_template_test_with_template(self): bp = flask.Blueprint('bp', __name__) @@ -730,7 +730,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -743,7 +743,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_add_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -756,7 +756,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 415d8aca..6633c6ba 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -28,7 +28,7 @@ class ConfigTestCase(FlaskTestCase): def common_object_test(self, app): self.assert_equal(app.secret_key, 'devkey') self.assert_equal(app.config['TEST_KEY'], 'foo') - self.assert_('ConfigTestCase' not in app.config) + self.assert_true('ConfigTestCase' not in app.config) def test_config_from_file(self): app = flask.Flask(__name__) @@ -57,13 +57,13 @@ class ConfigTestCase(FlaskTestCase): try: app.config.from_envvar('FOO_SETTINGS') except RuntimeError as e: - self.assert_("'FOO_SETTINGS' is not set" in str(e)) + self.assert_true("'FOO_SETTINGS' is not set" in str(e)) else: - self.assert_(0, 'expected exception') - self.assert_(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + self.assert_true(0, 'expected exception') + self.assert_true(not app.config.from_envvar('FOO_SETTINGS', silent=True)) os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} - self.assert_(app.config.from_envvar('FOO_SETTINGS')) + self.assert_true(app.config.from_envvar('FOO_SETTINGS')) self.common_object_test(app) finally: os.environ = env @@ -77,9 +77,9 @@ class ConfigTestCase(FlaskTestCase): app.config.from_envvar('FOO_SETTINGS') except IOError as e: msg = str(e) - self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + self.assert_true(msg.startswith('[Errno 2] Unable to load configuration ' 'file (No such file or directory):')) - self.assert_(msg.endswith("missing.cfg'")) + self.assert_true(msg.endswith("missing.cfg'")) else: self.fail('expected IOError') self.assertFalse(app.config.from_envvar('FOO_SETTINGS', silent=True)) @@ -92,12 +92,12 @@ class ConfigTestCase(FlaskTestCase): app.config.from_pyfile('missing.cfg') except IOError as e: msg = str(e) - self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + self.assert_true(msg.startswith('[Errno 2] Unable to load configuration ' 'file (No such file or directory):')) - self.assert_(msg.endswith("missing.cfg'")) + self.assert_true(msg.endswith("missing.cfg'")) else: - self.assert_(0, 'expected config') - self.assert_(not app.config.from_pyfile('missing.cfg', silent=True)) + self.assert_true(0, 'expected config') + self.assert_true(not app.config.from_pyfile('missing.cfg', silent=True)) def test_session_lifetime(self): app = flask.Flask(__name__) @@ -141,7 +141,7 @@ class InstanceTestCase(FlaskTestCase): try: flask.Flask(__name__, instance_path='instance') except ValueError as e: - self.assert_('must be absolute' in str(e)) + self.assert_true('must be absolute' in str(e)) else: self.fail('Expected value error') diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 147e23b0..a6d1be9d 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -42,7 +42,7 @@ class ExtImportHookTestCase(FlaskTestCase): def teardown(self): from flask import ext for key in ext.__dict__: - self.assert_('.' not in key) + self.assert_true('.' not in key) def test_flaskext_new_simple_import_normal(self): from flask.ext.newext_simple import ext_id @@ -108,12 +108,12 @@ class ExtImportHookTestCase(FlaskTestCase): import flask.ext.broken except ImportError: exc_type, exc_value, tb = sys.exc_info() - self.assert_(exc_type is ImportError) + self.assert_true(exc_type is ImportError) self.assert_equal(str(exc_value), 'No module named missing_module') - self.assert_(tb.tb_frame.f_globals is globals()) + self.assert_true(tb.tb_frame.f_globals is globals()) next = tb.tb_next - self.assert_('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) + self.assert_true('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) def suite(): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 9ca9eec3..702ab517 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -48,8 +48,8 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) self.assert_equal(rv.mimetype, 'application/json') - self.assert_('description' in flask.json.loads(rv.data)) - self.assert_('

    ' not in flask.json.loads(rv.data)['description']) + self.assert_true('description' in flask.json.loads(rv.data)) + self.assert_true('

    ' not in flask.json.loads(rv.data)['description']) def test_json_body_encoding(self): app = flask.Flask(__name__) @@ -167,7 +167,7 @@ class SendfileTestCase(FlaskTestCase): app = flask.Flask(__name__) with app.test_request_context(): rv = flask.send_file('static/index.html') - self.assert_(rv.direct_passthrough) + self.assert_true(rv.direct_passthrough) self.assert_equal(rv.mimetype, 'text/html') with app.open_resource('static/index.html') as f: self.assert_equal(rv.data, f.read()) @@ -177,8 +177,8 @@ class SendfileTestCase(FlaskTestCase): app.use_x_sendfile = True with app.test_request_context(): rv = flask.send_file('static/index.html') - self.assert_(rv.direct_passthrough) - self.assert_('x-sendfile' in rv.headers) + self.assert_true(rv.direct_passthrough) + self.assert_true('x-sendfile' in rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) self.assert_equal(rv.mimetype, 'text/html') @@ -201,7 +201,7 @@ class SendfileTestCase(FlaskTestCase): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) self.assert_equal(rv.mimetype, 'text/html') - self.assert_('x-sendfile' in rv.headers) + self.assert_true('x-sendfile' in rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) # mimetypes + etag @@ -229,7 +229,7 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): f = StringIO('Test') rv = flask.send_file(f) - self.assert_('x-sendfile' not in rv.headers) + self.assert_true('x-sendfile' not in rv.headers) # etags self.assert_equal(len(captured), 1) @@ -302,10 +302,10 @@ class LoggingTestCase(FlaskTestCase): def test_logger_cache(self): app = flask.Flask(__name__) logger1 = app.logger - self.assert_(app.logger is logger1) + self.assert_true(app.logger is logger1) self.assert_equal(logger1.name, __name__) app.logger_name = __name__ + '/test_logger_cache' - self.assert_(app.logger is not logger1) + self.assert_true(app.logger is not logger1) def test_debug_log(self): app = flask.Flask(__name__) @@ -325,10 +325,10 @@ class LoggingTestCase(FlaskTestCase): with catch_stderr() as err: c.get('/') out = err.getvalue() - self.assert_('WARNING in helpers [' in out) - self.assert_(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) - self.assert_('the standard library is dead' in out) - self.assert_('this is a debug statement' in out) + self.assert_true('WARNING in helpers [' in out) + self.assert_true(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) + self.assert_true('the standard library is dead' in out) + self.assert_true('this is a debug statement' in out) with catch_stderr() as err: try: @@ -336,7 +336,7 @@ class LoggingTestCase(FlaskTestCase): except ZeroDivisionError: pass else: - self.assert_(False, 'debug log ate the exception') + self.assert_true(False, 'debug log ate the exception') def test_debug_log_override(self): app = flask.Flask(__name__) @@ -357,13 +357,13 @@ class LoggingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_(b'Internal Server Error' in rv.data) + self.assert_true(b'Internal Server Error' in rv.data) err = out.getvalue() - self.assert_('Exception on / [GET]' in err) - self.assert_('Traceback (most recent call last):' in err) - self.assert_('1/0' in err) - self.assert_('ZeroDivisionError:' in err) + self.assert_true('Exception on / [GET]' in err) + self.assert_true('Traceback (most recent call last):' in err) + self.assert_true('1/0' in err) + self.assert_true('ZeroDivisionError:' in err) def test_processor_exceptions(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py index 6e379448..69b43f84 100644 --- a/flask/testsuite/reqctx.py +++ b/flask/testsuite/reqctx.py @@ -57,7 +57,7 @@ class RequestContextTestCase(FlaskTestCase): with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}): pass except Exception as e: - self.assert_(isinstance(e, ValueError)) + self.assert_true(isinstance(e, ValueError)) self.assert_equal(str(e), "the server name provided " + "('localhost.localdomain:5000') does not match the " + \ "server name from the WSGI environment ('localhost')") @@ -93,17 +93,17 @@ class RequestContextTestCase(FlaskTestCase): self.assert_equal(index(), 'Hello World!') with app.test_request_context('/meh'): self.assert_equal(meh(), 'http://localhost/meh') - self.assert_(flask._request_ctx_stack.top is None) + self.assert_true(flask._request_ctx_stack.top is None) def test_context_test(self): app = flask.Flask(__name__) - self.assert_(not flask.request) - self.assert_(not flask.has_request_context()) + self.assert_true(not flask.request) + self.assert_true(not flask.has_request_context()) ctx = app.test_request_context() ctx.push() try: - self.assert_(flask.request) - self.assert_(flask.has_request_context()) + self.assert_true(flask.request) + self.assert_true(flask.has_request_context()) finally: ctx.pop() @@ -122,7 +122,7 @@ class RequestContextTestCase(FlaskTestCase): except RuntimeError: pass else: - self.assert_(0, 'expected runtime error') + self.assert_true(0, 'expected runtime error') def test_greenlet_context_copying(self): app = flask.Flask(__name__) @@ -132,14 +132,14 @@ class RequestContextTestCase(FlaskTestCase): def index(): reqctx = flask._request_ctx_stack.top.copy() def g(): - self.assert_(not flask.request) - self.assert_(not flask.current_app) + self.assert_true(not flask.request) + self.assert_true(not flask.current_app) with reqctx: - self.assert_(flask.request) + self.assert_true(flask.request) self.assert_equal(flask.current_app, app) self.assert_equal(flask.request.path, '/') self.assert_equal(flask.request.args['foo'], 'bar') - self.assert_(not flask.request) + self.assert_true(not flask.request) return 42 greenlets.append(greenlet(g)) return 'Hello World!' @@ -159,7 +159,7 @@ class RequestContextTestCase(FlaskTestCase): reqctx = flask._request_ctx_stack.top.copy() @flask.copy_current_request_context def g(): - self.assert_(flask.request) + self.assert_true(flask.request) self.assert_equal(flask.current_app, app) self.assert_equal(flask.request.path, '/') self.assert_equal(flask.request.args['foo'], 'bar') diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index 843c2f83..ffd575c1 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -92,7 +92,7 @@ class SignalsTestCase(FlaskTestCase): try: self.assert_equal(app.test_client().get('/').status_code, 500) self.assert_equal(len(recorded), 1) - self.assert_(isinstance(recorded[0], ZeroDivisionError)) + self.assert_true(isinstance(recorded[0], ZeroDivisionError)) finally: flask.got_request_exception.disconnect(record, app) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index e26b9085..a3cf5d0e 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -34,7 +34,7 @@ class FlaskSubclassingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_(b'Internal Server Error' in rv.data) + self.assert_true(b'Internal Server Error' in rv.data) err = out.getvalue() self.assert_equal(err, '') diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 0d3a2a5c..08a54d06 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -99,7 +99,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter() def my_reverse(s): return s[::-1] - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_true('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -108,7 +108,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse) - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_true('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -117,7 +117,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter('strrev') def my_reverse(s): return s[::-1] - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_true('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -126,7 +126,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, 'strrev') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_true('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -179,36 +179,36 @@ class TemplatingTestCase(FlaskTestCase): @app.template_test() def boolean(value): return isinstance(value, bool) - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_add_template_test(self): app = flask.Flask(__name__) def boolean(value): return isinstance(value, bool) app.add_template_test(boolean) - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_template_test_with_name(self): app = flask.Flask(__name__) @app.template_test('boolean') def is_boolean(value): return isinstance(value, bool) - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_add_template_test_with_name(self): app = flask.Flask(__name__) def is_boolean(value): return isinstance(value, bool) app.add_template_test(is_boolean, 'boolean') - self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_true('boolean' in app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) - self.assert_(app.jinja_env.tests['boolean'](False)) + self.assert_true(app.jinja_env.tests['boolean'](False)) def test_template_test_with_template(self): app = flask.Flask(__name__) @@ -219,7 +219,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_add_template_test_with_template(self): app = flask.Flask(__name__) @@ -230,7 +230,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -241,7 +241,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_add_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -252,16 +252,16 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_(b'Success!' in rv.data) + self.assert_true(b'Success!' in rv.data) def test_add_template_global(self): app = flask.Flask(__name__) @app.template_global() def get_stuff(): return 42 - self.assert_('get_stuff' in app.jinja_env.globals.keys()) + self.assert_true('get_stuff' in app.jinja_env.globals.keys()) self.assert_equal(app.jinja_env.globals['get_stuff'], get_stuff) - self.assert_(app.jinja_env.globals['get_stuff'](), 42) + self.assert_true(app.jinja_env.globals['get_stuff'](), 42) with app.app_context(): rv = flask.render_template_string('{{ get_stuff() }}') self.assert_equal(rv, '42') diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 3379a463..a18db673 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -106,7 +106,7 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction() as sess: pass except RuntimeError as e: - self.assert_('Session backend did not open a session' in str(e)) + self.assert_true('Session backend did not open a session' in str(e)) else: self.fail('Expected runtime error') @@ -118,9 +118,9 @@ class TestToolsTestCase(FlaskTestCase): with app.test_client() as c: rv = c.get('/') req = flask.request._get_current_object() - self.assert_(req is not None) + self.assert_true(req is not None) with c.session_transaction(): - self.assert_(req is flask.request._get_current_object()) + self.assert_true(req is flask.request._get_current_object()) def test_session_transaction_needs_cookies(self): app = flask.Flask(__name__) @@ -130,7 +130,7 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction() as s: pass except RuntimeError as e: - self.assert_('cookies' in str(e)) + self.assert_true('cookies' in str(e)) else: self.fail('Expected runtime error') @@ -152,8 +152,8 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(resp.status_code, 200) resp = c.get('/other') - self.assert_(not hasattr(flask.g, 'value')) - self.assert_(b'Internal Server Error' in resp.data) + self.assert_true(not hasattr(flask.g, 'value')) + self.assert_true(b'Internal Server Error' in resp.data) self.assert_equal(resp.status_code, 500) flask.g.value = 23 From 8e9f0bdedca3edd5ba2e8902f05406acff8b8b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 17:30:37 +0200 Subject: [PATCH 1220/3747] Use assert_equal instead of assertEquals assertEquals is deprecated and its use inconsistent --- flask/testsuite/testing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index a18db673..2e735c47 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -219,8 +219,8 @@ class SubdomainTestCase(FlaskTestCase): url = flask.url_for('view', company_id='xxx') response = self.client.get(url) - self.assertEquals(200, response.status_code) - self.assertEquals(b'xxx', response.data) + self.assert_equal(200, response.status_code) + self.assert_equal(b'xxx', response.data) def test_nosubdomain(self): @@ -231,8 +231,8 @@ class SubdomainTestCase(FlaskTestCase): url = flask.url_for('view', company_id='xxx') response = self.client.get(url) - self.assertEquals(200, response.status_code) - self.assertEquals(b'xxx', response.data) + self.assert_equal(200, response.status_code) + self.assert_equal(b'xxx', response.data) def suite(): From 4d73ef1a194a4280d437db5cbc9ceb8850e99e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 19:19:46 +0200 Subject: [PATCH 1221/3747] Add missing msg argument to assert_true() --- flask/testsuite/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 2a869173..9088fc52 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -142,8 +142,8 @@ class FlaskTestCase(unittest.TestCase): with catcher: callable(*args, **kwargs) - def assert_true(self, x): - self.assertTrue(x) + def assert_true(self, x, msg=None): + self.assertTrue(x, msg) class _ExceptionCatcher(object): From 9f8a2075c79fec1b5481f5a5a95bb23b40bf68f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 20:12:50 +0200 Subject: [PATCH 1222/3747] Use assert_in where appropriate --- flask/testsuite/__init__.py | 8 ++++- flask/testsuite/basic.py | 54 +++++++++++++++++----------------- flask/testsuite/blueprints.py | 26 ++++++++-------- flask/testsuite/config.py | 4 +-- flask/testsuite/ext.py | 4 +-- flask/testsuite/helpers.py | 28 +++++++++--------- flask/testsuite/subclassing.py | 2 +- flask/testsuite/templating.py | 26 ++++++++-------- flask/testsuite/testing.py | 6 ++-- 9 files changed, 82 insertions(+), 76 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 9088fc52..72de1963 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -103,7 +103,7 @@ def emits_module_deprecation_warning(f): f(self, *args, **kwargs) self.assert_true(log, 'expected deprecation warning') for entry in log: - self.assert_true('Modules are deprecated' in str(entry['message'])) + self.assert_in('Modules are deprecated', str(entry['message'])) return update_wrapper(new_f, f) @@ -145,6 +145,12 @@ class FlaskTestCase(unittest.TestCase): def assert_true(self, x, msg=None): self.assertTrue(x, msg) + def assert_in(self, x, y): + self.assertIn(x, y) + + def assert_not_in(self, x, y): + self.assertNotIn(x, y) + class _ExceptionCatcher(object): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 20c2f0cc..0abcac10 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -173,8 +173,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com/') - self.assert_true('domain=.example.com' in rv.headers['set-cookie'].lower()) - self.assert_true('httponly' in rv.headers['set-cookie'].lower()) + self.assert_in('domain=.example.com', rv.headers['set-cookie'].lower()) + self.assert_in('httponly', rv.headers['set-cookie'].lower()) def test_session_using_server_name_and_port(self): app = flask.Flask(__name__) @@ -187,8 +187,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - self.assert_true('domain=.example.com' in rv.headers['set-cookie'].lower()) - self.assert_true('httponly' in rv.headers['set-cookie'].lower()) + self.assert_in('domain=.example.com', rv.headers['set-cookie'].lower()) + self.assert_in('httponly', rv.headers['set-cookie'].lower()) def test_session_using_server_name_port_and_path(self): app = flask.Flask(__name__) @@ -202,9 +202,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/foo') - self.assert_true('domain=example.com' in rv.headers['set-cookie'].lower()) - self.assert_true('path=/foo' in rv.headers['set-cookie'].lower()) - self.assert_true('httponly' in rv.headers['set-cookie'].lower()) + self.assert_in('domain=example.com', rv.headers['set-cookie'].lower()) + self.assert_in('path=/foo', rv.headers['set-cookie'].lower()) + self.assert_in('httponly', rv.headers['set-cookie'].lower()) def test_session_using_application_root(self): class PrefixPathMiddleware(object): @@ -226,7 +226,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): flask.session['testing'] = 42 return 'Hello World' rv = app.test_client().get('/', 'http://example.com:8080/') - self.assert_true('path=/bar' in rv.headers['set-cookie'].lower()) + self.assert_in('path=/bar', rv.headers['set-cookie'].lower()) def test_session_using_session_settings(self): app = flask.Flask(__name__) @@ -245,10 +245,10 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'Hello World' rv = app.test_client().get('/', 'http://www.example.com:8080/test/') cookie = rv.headers['set-cookie'].lower() - self.assert_true('domain=.example.com' in cookie) - self.assert_true('path=/;' in cookie) - self.assert_true('secure' in cookie) - self.assert_true('httponly' not in cookie) + self.assert_in('domain=.example.com', cookie) + self.assert_in('path=/;', cookie) + self.assert_in('secure', cookie) + self.assert_not_in('httponly', cookie) def test_missing_session(self): app = flask.Flask(__name__) @@ -280,7 +280,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): client = app.test_client() rv = client.get('/') - self.assert_true('set-cookie' in rv.headers) + self.assert_in('set-cookie', rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime @@ -293,7 +293,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): permanent = False rv = app.test_client().get('/') - self.assert_true('set-cookie' in rv.headers) + self.assert_in('set-cookie', rv.headers) match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) self.assert_true(match is None) @@ -448,12 +448,12 @@ class BasicFunctionalityTestCase(FlaskTestCase): return response @app.route('/') def index(): - self.assert_true('before' in evts) - self.assert_true('after' not in evts) + self.assert_in('before', evts) + self.assert_not_in('after', evts) return 'request' - self.assert_true('after' not in evts) + self.assert_not_in('after', evts) rv = app.test_client().get('/').data - self.assert_true(b'after' in evts) + self.assert_in(b'after', evts) self.assert_equal(rv, b'request|after') def test_after_request_processing(self): @@ -483,7 +483,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_true(b'Response' in rv.data) + self.assert_in(b'Response', rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_debug_mode(self): @@ -499,7 +499,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return "Response" rv = app.test_client().get('/') self.assert_equal(rv.status_code, 200) - self.assert_true(b'Response' in rv.data) + self.assert_in(b'Response', rv.data) self.assert_equal(len(called), 1) def test_teardown_request_handler_error(self): @@ -532,7 +532,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): 1/0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_true(b'Internal Server Error' in rv.data) + self.assert_in(b'Internal Server Error', rv.data) self.assert_equal(len(called), 2) def test_before_after_request_order(self): @@ -664,8 +664,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.post('/fail', data={'foo': 'index.txt'}) except DebugFilesKeyError as e: - self.assert_true('no file contents were transmitted' in str(e)) - self.assert_true('This was submitted: "index.txt"' in str(e)) + self.assert_in('no file contents were transmitted', str(e)) + self.assert_in('This was submitted: "index.txt"', str(e)) else: self.fail('Expected exception') @@ -974,7 +974,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def broken(): return 'Meh' except AssertionError as e: - self.assert_true('A setup function was called' in str(e)) + self.assert_in('A setup function was called', str(e)) else: self.fail('Expected exception') @@ -1008,9 +1008,9 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: c.post('/foo', data={}) except AssertionError as e: - self.assert_true('http://localhost/foo/' in str(e)) - self.assert_true('Make sure to directly send your POST-request ' - 'to this URL' in str(e)) + self.assert_in('http://localhost/foo/', str(e)) + self.assert_in('Make sure to directly send your POST-request ' + 'to this URL', str(e)) else: self.fail('Expected exception') diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 088c72d2..89a8f2d7 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -547,7 +547,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('my_reverse' in app.jinja_env.filters.keys()) + self.assert_in('my_reverse', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -558,7 +558,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('my_reverse' in app.jinja_env.filters.keys()) + self.assert_in('my_reverse', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -569,7 +569,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('strrev' in app.jinja_env.filters.keys()) + self.assert_in('strrev', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -580,7 +580,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse, 'strrev') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('strrev' in app.jinja_env.filters.keys()) + self.assert_in('strrev', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -656,7 +656,7 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('is_boolean' in app.jinja_env.tests.keys()) + self.assert_in('is_boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) self.assert_true(app.jinja_env.tests['is_boolean'](False)) @@ -667,7 +667,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_test(is_boolean) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('is_boolean' in app.jinja_env.tests.keys()) + self.assert_in('is_boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) self.assert_true(app.jinja_env.tests['is_boolean'](False)) @@ -678,7 +678,7 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -689,7 +689,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_test(is_boolean, 'boolean') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -704,7 +704,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_template_test_after_route_with_template(self): app = flask.Flask(__name__) @@ -717,7 +717,7 @@ class BlueprintTestCase(FlaskTestCase): return isinstance(value, bool) app.register_blueprint(bp, url_prefix='/py') rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_add_template_test_with_template(self): bp = flask.Blueprint('bp', __name__) @@ -730,7 +730,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -743,7 +743,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_add_template_test_with_name_and_template(self): bp = flask.Blueprint('bp', __name__) @@ -756,7 +756,7 @@ class BlueprintTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 6633c6ba..8aaf5285 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -28,7 +28,7 @@ class ConfigTestCase(FlaskTestCase): def common_object_test(self, app): self.assert_equal(app.secret_key, 'devkey') self.assert_equal(app.config['TEST_KEY'], 'foo') - self.assert_true('ConfigTestCase' not in app.config) + self.assert_not_in('ConfigTestCase', app.config) def test_config_from_file(self): app = flask.Flask(__name__) @@ -141,7 +141,7 @@ class InstanceTestCase(FlaskTestCase): try: flask.Flask(__name__, instance_path='instance') except ValueError as e: - self.assert_true('must be absolute' in str(e)) + self.assert_in('must be absolute', str(e)) else: self.fail('Expected value error') diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index a6d1be9d..04226181 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -42,7 +42,7 @@ class ExtImportHookTestCase(FlaskTestCase): def teardown(self): from flask import ext for key in ext.__dict__: - self.assert_true('.' not in key) + self.assert_not_in('.', key) def test_flaskext_new_simple_import_normal(self): from flask.ext.newext_simple import ext_id @@ -113,7 +113,7 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_true(tb.tb_frame.f_globals is globals()) next = tb.tb_next - self.assert_true('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename) + self.assert_in('flask_broken/__init__.py', next.tb_frame.f_code.co_filename) def suite(): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 702ab517..b76d36d1 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -48,8 +48,8 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) self.assert_equal(rv.mimetype, 'application/json') - self.assert_true('description' in flask.json.loads(rv.data)) - self.assert_true('

    ' not in flask.json.loads(rv.data)['description']) + self.assert_in('description', flask.json.loads(rv.data)) + self.assert_not_in('

    ', flask.json.loads(rv.data)['description']) def test_json_body_encoding(self): app = flask.Flask(__name__) @@ -178,7 +178,7 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): rv = flask.send_file('static/index.html') self.assert_true(rv.direct_passthrough) - self.assert_true('x-sendfile' in rv.headers) + self.assert_in('x-sendfile', rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) self.assert_equal(rv.mimetype, 'text/html') @@ -201,7 +201,7 @@ class SendfileTestCase(FlaskTestCase): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) self.assert_equal(rv.mimetype, 'text/html') - self.assert_true('x-sendfile' in rv.headers) + self.assert_in('x-sendfile', rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) # mimetypes + etag @@ -229,7 +229,7 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): f = StringIO('Test') rv = flask.send_file(f) - self.assert_true('x-sendfile' not in rv.headers) + self.assert_not_in('x-sendfile', rv.headers) # etags self.assert_equal(len(captured), 1) @@ -325,10 +325,10 @@ class LoggingTestCase(FlaskTestCase): with catch_stderr() as err: c.get('/') out = err.getvalue() - self.assert_true('WARNING in helpers [' in out) - self.assert_true(os.path.basename(__file__.rsplit('.', 1)[0] + '.py') in out) - self.assert_true('the standard library is dead' in out) - self.assert_true('this is a debug statement' in out) + self.assert_in('WARNING in helpers [', out) + self.assert_in(os.path.basename(__file__.rsplit('.', 1)[0] + '.py'), out) + self.assert_in('the standard library is dead', out) + self.assert_in('this is a debug statement', out) with catch_stderr() as err: try: @@ -357,13 +357,13 @@ class LoggingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_true(b'Internal Server Error' in rv.data) + self.assert_in(b'Internal Server Error', rv.data) err = out.getvalue() - self.assert_true('Exception on / [GET]' in err) - self.assert_true('Traceback (most recent call last):' in err) - self.assert_true('1/0' in err) - self.assert_true('ZeroDivisionError:' in err) + self.assert_in('Exception on / [GET]', err) + self.assert_in('Traceback (most recent call last):', err) + self.assert_in('1/0', err) + self.assert_in('ZeroDivisionError:', err) def test_processor_exceptions(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index a3cf5d0e..dbfdd499 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -34,7 +34,7 @@ class FlaskSubclassingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) - self.assert_true(b'Internal Server Error' in rv.data) + self.assert_in(b'Internal Server Error', rv.data) err = out.getvalue() self.assert_equal(err, '') diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 08a54d06..b2870dea 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -99,7 +99,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter() def my_reverse(s): return s[::-1] - self.assert_true('my_reverse' in app.jinja_env.filters.keys()) + self.assert_in('my_reverse', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -108,7 +108,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse) - self.assert_true('my_reverse' in app.jinja_env.filters.keys()) + self.assert_in('my_reverse', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -117,7 +117,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter('strrev') def my_reverse(s): return s[::-1] - self.assert_true('strrev' in app.jinja_env.filters.keys()) + self.assert_in('strrev', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -126,7 +126,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, 'strrev') - self.assert_true('strrev' in app.jinja_env.filters.keys()) + self.assert_in('strrev', app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -179,7 +179,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_test() def boolean(value): return isinstance(value, bool) - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -188,7 +188,7 @@ class TemplatingTestCase(FlaskTestCase): def boolean(value): return isinstance(value, bool) app.add_template_test(boolean) - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -197,7 +197,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_test('boolean') def is_boolean(value): return isinstance(value, bool) - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -206,7 +206,7 @@ class TemplatingTestCase(FlaskTestCase): def is_boolean(value): return isinstance(value, bool) app.add_template_test(is_boolean, 'boolean') - self.assert_true('boolean' in app.jinja_env.tests.keys()) + self.assert_in('boolean', app.jinja_env.tests.keys()) self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) self.assert_true(app.jinja_env.tests['boolean'](False)) @@ -219,7 +219,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_add_template_test_with_template(self): app = flask.Flask(__name__) @@ -230,7 +230,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -241,7 +241,7 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_add_template_test_with_name_and_template(self): app = flask.Flask(__name__) @@ -252,14 +252,14 @@ class TemplatingTestCase(FlaskTestCase): def index(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') - self.assert_true(b'Success!' in rv.data) + self.assert_in(b'Success!', rv.data) def test_add_template_global(self): app = flask.Flask(__name__) @app.template_global() def get_stuff(): return 42 - self.assert_true('get_stuff' in app.jinja_env.globals.keys()) + self.assert_in('get_stuff', app.jinja_env.globals.keys()) self.assert_equal(app.jinja_env.globals['get_stuff'], get_stuff) self.assert_true(app.jinja_env.globals['get_stuff'](), 42) with app.app_context(): diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 2e735c47..a60109e1 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -106,7 +106,7 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction() as sess: pass except RuntimeError as e: - self.assert_true('Session backend did not open a session' in str(e)) + self.assert_in('Session backend did not open a session', str(e)) else: self.fail('Expected runtime error') @@ -130,7 +130,7 @@ class TestToolsTestCase(FlaskTestCase): with c.session_transaction() as s: pass except RuntimeError as e: - self.assert_true('cookies' in str(e)) + self.assert_in('cookies', str(e)) else: self.fail('Expected runtime error') @@ -153,7 +153,7 @@ class TestToolsTestCase(FlaskTestCase): resp = c.get('/other') self.assert_true(not hasattr(flask.g, 'value')) - self.assert_true(b'Internal Server Error' in resp.data) + self.assert_in(b'Internal Server Error', resp.data) self.assert_equal(resp.status_code, 500) flask.g.value = 23 From 62e7275bdf2bce19eb9fcfd31780e9314f96f092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 20:17:29 +0200 Subject: [PATCH 1223/3747] Use assert_false where appropriate --- flask/testsuite/__init__.py | 3 +++ flask/testsuite/basic.py | 8 ++++---- flask/testsuite/config.py | 4 ++-- flask/testsuite/reqctx.py | 10 +++++----- flask/testsuite/testing.py | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 72de1963..88cd4d88 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -145,6 +145,9 @@ class FlaskTestCase(unittest.TestCase): def assert_true(self, x, msg=None): self.assertTrue(x, msg) + def assert_false(self, x, msg=None): + self.assertFalse(x, msg) + def assert_in(self, x, y): self.assertIn(x, y) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 0abcac10..85b758b1 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -78,7 +78,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - self.assert_true(not rv.data) # head truncates + self.assert_false(rv.data) # head truncates self.assert_equal(c.post('/more').data, b'POST') self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') @@ -102,7 +102,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS']) rv = c.head('/') self.assert_equal(rv.status_code, 200) - self.assert_true(not rv.data) # head truncates + self.assert_false(rv.data) # head truncates self.assert_equal(c.post('/more').data, b'POST') self.assert_equal(c.get('/more').data, b'GET') rv = c.delete('/more') @@ -347,7 +347,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.secret_key = 'testkey' with app.test_request_context(): - self.assert_true(not flask.session.modified) + self.assert_false(flask.session.modified) flask.flash('Zap') flask.session.modified = False flask.flash('Zip') @@ -967,7 +967,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/') def index(): return 'Awesome' - self.assert_true(not app.got_first_request) + self.assert_false(app.got_first_request) self.assert_equal(app.test_client().get('/').data, b'Awesome') try: @app.route('/foo') diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 8aaf5285..477c6db9 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -60,7 +60,7 @@ class ConfigTestCase(FlaskTestCase): self.assert_true("'FOO_SETTINGS' is not set" in str(e)) else: self.assert_true(0, 'expected exception') - self.assert_true(not app.config.from_envvar('FOO_SETTINGS', silent=True)) + self.assert_false(app.config.from_envvar('FOO_SETTINGS', silent=True)) os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} self.assert_true(app.config.from_envvar('FOO_SETTINGS')) @@ -97,7 +97,7 @@ class ConfigTestCase(FlaskTestCase): self.assert_true(msg.endswith("missing.cfg'")) else: self.assert_true(0, 'expected config') - self.assert_true(not app.config.from_pyfile('missing.cfg', silent=True)) + self.assert_false(app.config.from_pyfile('missing.cfg', silent=True)) def test_session_lifetime(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/reqctx.py b/flask/testsuite/reqctx.py index 69b43f84..c232a74c 100644 --- a/flask/testsuite/reqctx.py +++ b/flask/testsuite/reqctx.py @@ -97,8 +97,8 @@ class RequestContextTestCase(FlaskTestCase): def test_context_test(self): app = flask.Flask(__name__) - self.assert_true(not flask.request) - self.assert_true(not flask.has_request_context()) + self.assert_false(flask.request) + self.assert_false(flask.has_request_context()) ctx = app.test_request_context() ctx.push() try: @@ -132,14 +132,14 @@ class RequestContextTestCase(FlaskTestCase): def index(): reqctx = flask._request_ctx_stack.top.copy() def g(): - self.assert_true(not flask.request) - self.assert_true(not flask.current_app) + self.assert_false(flask.request) + self.assert_false(flask.current_app) with reqctx: self.assert_true(flask.request) self.assert_equal(flask.current_app, app) self.assert_equal(flask.request.path, '/') self.assert_equal(flask.request.args['foo'], 'bar') - self.assert_true(not flask.request) + self.assert_false(flask.request) return 42 greenlets.append(greenlet(g)) return 'Hello World!' diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index a60109e1..e7206d21 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -152,7 +152,7 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(resp.status_code, 200) resp = c.get('/other') - self.assert_true(not hasattr(flask.g, 'value')) + self.assert_false(hasattr(flask.g, 'value')) self.assert_in(b'Internal Server Error', resp.data) self.assert_equal(resp.status_code, 500) flask.g.value = 23 From 8f73c552a96cc54e6abefcfc0c4e9d4cdd7dc040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 20:53:32 +0200 Subject: [PATCH 1224/3747] Add missing assertIn, assertNotIn methods on 2.6 --- flask/testsuite/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 88cd4d88..89b8cec1 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -154,6 +154,13 @@ class FlaskTestCase(unittest.TestCase): def assert_not_in(self, x, y): self.assertNotIn(x, y) + if sys.version_info[:2] == (2, 6): + def assertIn(self, x, y): + assert x in y, "%r unexpectedly not in %r" % (x, y) + + def assertNotIn(self, x, y): + assert x not in y, "%r unexpectedly in %r" % (x, y) + class _ExceptionCatcher(object): From 3f80b0fd6c054d924c41a3c91152ec1984d8c28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 21:03:01 +0200 Subject: [PATCH 1225/3747] module name is quoted in ImportErrors on 3.x --- flask/testsuite/ext.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 04226181..f6209c67 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -12,6 +12,7 @@ import sys import unittest from flask.testsuite import FlaskTestCase +from flask._compat import PY2 from six.moves import reload_module class ExtImportHookTestCase(FlaskTestCase): @@ -109,7 +110,11 @@ class ExtImportHookTestCase(FlaskTestCase): except ImportError: exc_type, exc_value, tb = sys.exc_info() self.assert_true(exc_type is ImportError) - self.assert_equal(str(exc_value), 'No module named missing_module') + if PY2: + message = 'No module named missing_module' + else: + message = 'No module named \'missing_module\'' + self.assert_equal(str(exc_value), message) self.assert_true(tb.tb_frame.f_globals is globals()) next = tb.tb_next From 4bea6bbe6d2200dfdbcca3dfc2fe561488c82a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 21:07:53 +0200 Subject: [PATCH 1226/3747] Make DebugFilesKeyError.__str__ return str on 3.x --- flask/debughelpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 504fab93..f3bac185 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -8,6 +8,7 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from flask._compat import implements_to_string class UnexpectedUnicodeError(AssertionError, UnicodeError): @@ -16,6 +17,7 @@ class UnexpectedUnicodeError(AssertionError, UnicodeError): """ +@implements_to_string class DebugFilesKeyError(KeyError, AssertionError): """Raised from request.files during debugging. The idea is that it can provide a better error message than just a generic KeyError/BadRequest. @@ -33,7 +35,7 @@ class DebugFilesKeyError(KeyError, AssertionError): buf.append('\n\nThe browser instead transmitted some file names. ' 'This was submitted: %s' % ', '.join('"%s"' % x for x in form_matches)) - self.msg = ''.join(buf).encode('utf-8') + self.msg = ''.join(buf) def __str__(self): return self.msg From 3d36d6efb9371be92f6f22c844ed6a17c87f4a88 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 22 May 2013 21:09:32 +0200 Subject: [PATCH 1227/3747] Fix leak in leak detection code If ensure_clean_request_context found a leak, it would raise an AssertionError and not clean up the leak, and therefore affect other testcases. --- flask/testsuite/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/__init__.py b/flask/testsuite/__init__.py index 88cd4d88..5f5935cd 100644 --- a/flask/testsuite/__init__.py +++ b/flask/testsuite/__init__.py @@ -116,7 +116,10 @@ class FlaskTestCase(unittest.TestCase): def ensure_clean_request_context(self): # make sure we're not leaking a request context since we are # testing flask internally in debug mode in a few cases - self.assert_equal(flask._request_ctx_stack.top, None) + leaks = [] + while flask._request_ctx_stack.top is not None: + leaks.append(flask._request_ctx_stack.pop()) + self.assert_equal(leaks, []) def setup(self): pass From 135c53a5f2f990512d2be348dc16ef719233a314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 21:30:45 +0200 Subject: [PATCH 1228/3747] Fix .iteritems() access in flask.sessions --- flask/sessions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 04b7f8af..0b35b1c2 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -15,9 +15,9 @@ from datetime import datetime from werkzeug.http import http_date, parse_date from werkzeug.datastructures import CallbackDict from . import Markup, json +from ._compat import iteritems, text_type from itsdangerous import URLSafeTimedSerializer, BadSignature -import six def total_seconds(td): @@ -63,16 +63,16 @@ class TaggedJSONSerializer(object): elif isinstance(value, uuid.UUID): return {' u': value.hex} elif callable(getattr(value, '__html__', None)): - return {' m': six.text_type(value.__html__())} + return {' m': text_type(value.__html__())} elif isinstance(value, list): return [_tag(x) for x in value] elif isinstance(value, datetime): return {' d': http_date(value)} elif isinstance(value, dict): - return dict((k, _tag(v)) for k, v in six.iteritems(value)) + return dict((k, _tag(v)) for k, v in iteritems(value)) elif isinstance(value, str): try: - return six.text_type(value) + return text_type(value) except UnicodeError: raise UnexpectedUnicodeError(u'A byte string with ' u'non-ASCII data was passed to the session system ' @@ -85,7 +85,7 @@ class TaggedJSONSerializer(object): def object_hook(obj): if len(obj) != 1: return obj - the_key, the_value = six.advance_iterator(obj.iteritems()) + the_key, the_value = next(iteritems(obj)) if the_key == ' t': return tuple(the_value) elif the_key == ' u': From a0801719f8f24a0e3192eca203bbd341c4e557ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 21:40:30 +0200 Subject: [PATCH 1229/3747] Remove six dependency --- flask/helpers.py | 3 +-- flask/json.py | 4 ++-- flask/templating.py | 6 +++--- flask/testsuite/basic.py | 4 ++-- flask/testsuite/blueprints.py | 4 ++-- flask/testsuite/ext.py | 5 ++++- flask/testsuite/helpers.py | 11 +++++------ flask/testsuite/testing.py | 4 ++-- setup.py | 1 - 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 1359bba6..dbbbf2e6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -24,7 +24,6 @@ from functools import update_wrapper from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound -import six from flask._compat import string_types, text_type # this was moved in 0.7 @@ -128,7 +127,7 @@ def stream_with_context(generator_or_function): # pushed. This item is discarded. Then when the iteration continues the # real generator is executed. wrapped_g = generator() - six.advance_iterator(wrapped_g) + next(wrapped_g) return wrapped_g diff --git a/flask/json.py b/flask/json.py index 6b95fde0..7777ba5d 100644 --- a/flask/json.py +++ b/flask/json.py @@ -11,13 +11,13 @@ import uuid from datetime import datetime from .globals import current_app, request +from ._compat import text_type from werkzeug.http import http_date # Use the same json implementation as itsdangerous on which we # depend anyways. from itsdangerous import json as _json -import six # figure out if simplejson escapes slashes. This behavior was changed @@ -60,7 +60,7 @@ class JSONEncoder(_json.JSONEncoder): if isinstance(o, uuid.UUID): return str(o) if hasattr(o, '__html__'): - return six.text_type(o.__html__()) + return text_type(o.__html__()) return _json.JSONEncoder.default(self, o) diff --git a/flask/templating.py b/flask/templating.py index 754c6893..63adb092 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -15,7 +15,7 @@ from jinja2 import BaseLoader, Environment as BaseEnvironment, \ from .globals import _request_ctx_stack, _app_ctx_stack from .signals import template_rendered from .module import blueprint_is_module -import six +from ._compat import itervalues, iteritems def _default_template_ctx_processor(): @@ -80,7 +80,7 @@ class DispatchingJinjaLoader(BaseLoader): except (ValueError, KeyError): pass - for blueprint in six.itervalues(self.app.blueprints): + for blueprint in itervalues(self.app.blueprints): if blueprint_is_module(blueprint): continue loader = blueprint.jinja_loader @@ -93,7 +93,7 @@ class DispatchingJinjaLoader(BaseLoader): if loader is not None: result.update(loader.list_templates()) - for name, blueprint in six.iteritems(self.app.blueprints): + for name, blueprint in iteritems(self.app.blueprints): loader = blueprint.jinja_loader if loader is not None: for template in loader.list_templates(): diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 85b758b1..485ed5fd 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -17,10 +17,10 @@ import unittest from datetime import datetime from threading import Thread from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from flask._compat import text_type from werkzeug.exceptions import BadRequest, NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError -import six class BasicFunctionalityTestCase(FlaskTestCase): @@ -276,7 +276,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/test') def test(): - return six.text_type(flask.session.permanent) + return text_type(flask.session.permanent) client = app.test_client() rv = client.get('/') diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 89a8f2d7..97a196d7 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -13,10 +13,10 @@ import flask import unittest import warnings from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning +from flask._compat import text_type from werkzeug.exceptions import NotFound from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound -import six # import moduleapp here because it uses deprecated features and we don't @@ -303,7 +303,7 @@ class BlueprintTestCase(FlaskTestCase): @bp.route('/bar') def bar(bar): - return six.text_type(bar) + return text_type(bar) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index f6209c67..370a31d8 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -11,9 +11,12 @@ import sys import unittest +try: + from imp import reload as reload_module +except ImportError: + reload_module = reload from flask.testsuite import FlaskTestCase from flask._compat import PY2 -from six.moves import reload_module class ExtImportHookTestCase(FlaskTestCase): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index b76d36d1..d0054de1 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -15,8 +15,7 @@ import unittest from logging import StreamHandler from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr from werkzeug.http import parse_cache_control_header, parse_options_header -import six -from flask._compat import StringIO +from flask._compat import StringIO, text_type def has_encoding(name): @@ -34,7 +33,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/json', methods=['POST']) def return_json(): - return six.text_type(flask.request.json) + return text_type(flask.request.json) c = app.test_client() rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) @@ -43,7 +42,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/json', methods=['POST']) def return_json(): - return six.text_type(flask.request.json) + return text_type(flask.request.json) c = app.test_client() rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) @@ -95,7 +94,7 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/add', methods=['POST']) def add(): - return six.text_type(flask.request.json['a'] + flask.request.json['b']) + return text_type(flask.request.json['a'] + flask.request.json['b']) c = app.test_client() rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), content_type='application/json') @@ -506,7 +505,7 @@ class StreamingTestCase(FlaskTestCase): def close(self): called.append(42) def next(self): - return six.advance_iterator(self._gen) + return next(self._gen) @app.route('/') def index(): def generate(): diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index e7206d21..cd96b497 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -12,7 +12,7 @@ import flask import unittest from flask.testsuite import FlaskTestCase -import six +from flask._compat import text_type class TestToolsTestCase(FlaskTestCase): @@ -84,7 +84,7 @@ class TestToolsTestCase(FlaskTestCase): @app.route('/') def index(): - return six.text_type(flask.session['foo']) + return text_type(flask.session['foo']) with app.test_client() as c: with c.session_transaction() as sess: diff --git a/setup.py b/setup.py index 1d3e36ad..ddc83251 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,6 @@ setup( zip_safe=False, platforms='any', install_requires=[ - 'six>=1.3.0', 'Werkzeug>=0.7', 'Jinja2>=2.4', 'itsdangerous>=0.17' From 43b6d0a6d062ace75cc2b0f7200b770eac23edcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 22:49:25 +0200 Subject: [PATCH 1230/3747] Ensure that config file is closed immediately --- flask/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/config.py b/flask/config.py index ddb113a5..4d9ac23a 100644 --- a/flask/config.py +++ b/flask/config.py @@ -125,7 +125,8 @@ class Config(dict): d = imp.new_module('config') d.__file__ = filename try: - exec(compile(open(filename).read(), filename, 'exec'), d.__dict__) + with open(filename) as config_file: + exec(compile(config_file.read(), filename, 'exec'), d.__dict__) except IOError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False From 79ec3d81c1c0e4db7eb800e69f2c7aab3d8d02b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Wed, 22 May 2013 22:58:12 +0200 Subject: [PATCH 1231/3747] Prevent UnboundLocalError in test_build_error_handler --- flask/testsuite/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 485ed5fd..656233b8 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -753,8 +753,8 @@ class BasicFunctionalityTestCase(FlaskTestCase): try: with app.test_request_context(): flask.url_for('spam') - except BuildError as error: - pass + except BuildError as err: + error = err try: raise RuntimeError('Test case where BuildError is not current.') except RuntimeError: From eb023bcfad7741248705f6715055ffe46928b7fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 13:46:51 +0100 Subject: [PATCH 1232/3747] Support old and new name for json --- flask/json.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flask/json.py b/flask/json.py index 717eb2ab..0f1d4c2a 100644 --- a/flask/json.py +++ b/flask/json.py @@ -15,8 +15,11 @@ from .globals import current_app, request from werkzeug.http import http_date # Use the same json implementation as itsdangerous on which we -# depend anyways. -from itsdangerous import simplejson as _json +# depend anyways. This name changed at one point so support both. +try: + from itsdangerous import simplejson as _json +except ImportError: + from itsdangerous import json as _json # figure out if simplejson escapes slashes. This behavior was changed From 4c27f7a8c4bbe6621681178be716e6270067a3ad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 13:59:10 +0100 Subject: [PATCH 1233/3747] Removed incorrect JSON exception subclasses --- CHANGES | 3 +++ flask/exceptions.py | 48 -------------------------------------- flask/testsuite/helpers.py | 14 +---------- flask/wrappers.py | 19 ++++++--------- 4 files changed, 11 insertions(+), 73 deletions(-) delete mode 100644 flask/exceptions.py diff --git a/CHANGES b/CHANGES index e383b56f..46b816a5 100644 --- a/CHANGES +++ b/CHANGES @@ -54,6 +54,9 @@ Release date to be decided. - Added `message_flashed` signal that simplifies flashing testing. - Added support for copying of request contexts for better working with greenlets. +- Removed custom JSON HTTP exception subclasses. If you were relying on them + you can reintroduce them again yourself trivially. Using them however is + strongly discouraged as the interface was flawed. Version 0.9 ----------- diff --git a/flask/exceptions.py b/flask/exceptions.py deleted file mode 100644 index 83b9556b..00000000 --- a/flask/exceptions.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.exceptions - ~~~~~~~~~~~~ - - Flask specific additions to :class:`~werkzeug.exceptions.HTTPException` - - :copyright: (c) 2011 by Armin Ronacher. - :license: BSD, see LICENSE for more details. -""" -from werkzeug.exceptions import HTTPException, BadRequest -from . import json - - -class JSONHTTPException(HTTPException): - """A base class for HTTP exceptions with ``Content-Type: - application/json``. - - The ``description`` attribute of this class must set to a string (*not* an - HTML string) which describes the error. - - """ - - def get_body(self, environ): - """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to - return the description of this error in JSON format instead of HTML. - - """ - return json.dumps(dict(description=self.get_description(environ))) - - def get_headers(self, environ): - """Returns a list of headers including ``Content-Type: - application/json``. - - """ - return [('Content-Type', 'application/json')] - - -class JSONBadRequest(JSONHTTPException, BadRequest): - """Represents an HTTP ``400 Bad Request`` error whose body contains an - error message in JSON format instead of HTML format (as in the superclass). - """ - - #: The description of the error which occurred as a string. - description = ( - 'The browser (or proxy) sent a request that this server could not ' - 'understand.' - ) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index fdf2d89f..8e471af9 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -35,23 +35,11 @@ class JSONTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/json', methods=['POST']) def return_json(): - return unicode(flask.request.json) + return flask.jsonify(foo=unicode(flask.request.json)) c = app.test_client() rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) - def test_json_bad_requests_content_type(self): - app = flask.Flask(__name__) - @app.route('/json', methods=['POST']) - def return_json(): - return unicode(flask.request.json) - c = app.test_client() - rv = c.post('/json', data='malformed', content_type='application/json') - self.assert_equal(rv.status_code, 400) - self.assert_equal(rv.mimetype, 'application/json') - self.assert_('description' in flask.json.loads(rv.data)) - self.assert_('

    ' not in flask.json.loads(rv.data)['description']) - def test_json_body_encoding(self): app = flask.Flask(__name__) app.testing = True diff --git a/flask/wrappers.py b/flask/wrappers.py index a56fe5d7..100decd0 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -11,8 +11,8 @@ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.utils import cached_property +from werkzeug.exceptions import BadRequest -from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict from . import json from .globals import _request_ctx_stack @@ -107,21 +107,16 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of this method is used by :attr:`json` when an error occurred. The default - implementation raises a :class:`JSONBadRequest`, which is a subclass of - :class:`~werkzeug.exceptions.BadRequest` which sets the - ``Content-Type`` to ``application/json`` and provides a JSON-formatted - error description:: + implementation just raises a :class:`BadRequest` exception. - {"description": "The browser (or proxy) sent a request that \ - this server could not understand."} - - .. versionchanged:: 0.9 - Return a :class:`JSONBadRequest` instead of a - :class:`~werkzeug.exceptions.BadRequest` by default. + .. versionchanged:: 0.10 + Removed buggy previous behavior of generating a random JSON + response. If you want that behavior back you can trivially + add it by subclassing. .. versionadded:: 0.8 """ - raise JSONBadRequest() + raise BadRequest() def _load_form_data(self): RequestBase._load_form_data(self) From 85ba8c96e9371368cff772e4e270cf685d015e03 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 14:07:25 +0100 Subject: [PATCH 1234/3747] Fixed a broken test --- flask/testsuite/ext.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 370a31d8..1a7a4a5a 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -120,7 +120,8 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_equal(str(exc_value), message) self.assert_true(tb.tb_frame.f_globals is globals()) - next = tb.tb_next + # reraise() adds a second frame so we need to skip that one too. + next = tb.tb_next.tb_next self.assert_in('flask_broken/__init__.py', next.tb_frame.f_code.co_filename) From 22816b1d3d6db4dbcecf8c07c8e54a0ad2a8d28b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 14:09:22 +0100 Subject: [PATCH 1235/3747] Removed 2.5 support in master --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd590bea..b0c10252 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,10 @@ language: python python: - - 2.5 - 2.6 - 2.7 - pypy -before_install: pip install simplejson - script: python setup.py test branches: From df5890ad715e35b58b7a00d202cb7145fee8418d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 14:15:27 +0100 Subject: [PATCH 1236/3747] Change travis test command --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b0c10252..6594edd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - 2.7 - pypy -script: python setup.py test +script: make test branches: except: From 250247d96c84b3bb3e45af5b8c1411aca2b35287 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 23 May 2013 14:17:51 +0100 Subject: [PATCH 1237/3747] Added install section --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6594edd5..4929600e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ python: - 2.7 - pypy +install: pip install --editable . + script: make test branches: From 12c08c03fb9e58d83094021a559329a7985e93c6 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 25 May 2013 09:10:41 +0300 Subject: [PATCH 1238/3747] Fixed typo in app.blueprints docstring --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index cc9a2f90..f04a9b07 100644 --- a/flask/app.py +++ b/flask/app.py @@ -452,7 +452,7 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } - #: all the attached blueprints in a directory by name. Blueprints + #: all the attached blueprints in a dictionary by name. Blueprints #: can be attached multiple times so this dictionary does not tell #: you how often they got attached. #: From 8bb972e5ae647650457bc4e94ff51cb5e34951dd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 19:13:48 +0200 Subject: [PATCH 1239/3747] fix minitwit/flaskr test errors, improve docs about file open mode app.open_resource needs to get called with the correct mode param (python3 will read bytes [not str] if the wrong mode is used), add mode param docs. rv.data is bytes, fix the data type we compare it with to be also bytes --- docs/patterns/sqlite3.rst | 2 +- docs/tutorial/dbinit.rst | 2 +- examples/flaskr/flaskr.py | 2 +- examples/flaskr/flaskr_tests.py | 16 ++++----- examples/minitwit/minitwit.py | 2 +- examples/minitwit/minitwit_tests.py | 56 ++++++++++++++--------------- flask/app.py | 1 + flask/helpers.py | 1 + 8 files changed, 42 insertions(+), 40 deletions(-) diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 76fec0b2..e625d15b 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -124,7 +124,7 @@ can do that for you:: def init_db(): with app.app_context(): db = get_db() - with app.open_resource('schema.sql') as f: + with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index b32a8eda..1241193a 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -33,7 +33,7 @@ earlier. Just add that function below the `connect_db` function in def init_db(): with closing(connect_db()) as db: - with app.open_resource('schema.sql') as f: + with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr.py index 20254660..b193e94e 100644 --- a/examples/flaskr/flaskr.py +++ b/examples/flaskr/flaskr.py @@ -31,7 +31,7 @@ def init_db(): """Creates the database tables.""" with app.app_context(): db = get_db() - with app.open_resource('schema.sql') as f: + with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() diff --git a/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index cfac3782..dd16c038 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -42,21 +42,21 @@ class FlaskrTestCase(unittest.TestCase): def test_empty_db(self): """Start with a blank database.""" rv = self.app.get('/') - assert 'No entries here so far' in rv.data + assert b'No entries here so far' in rv.data def test_login_logout(self): """Make sure login and logout works""" rv = self.login(flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) - assert 'You were logged in' in rv.data + assert b'You were logged in' in rv.data rv = self.logout() - assert 'You were logged out' in rv.data + assert b'You were logged out' in rv.data rv = self.login(flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD']) - assert 'Invalid username' in rv.data + assert b'Invalid username' in rv.data rv = self.login(flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x') - assert 'Invalid password' in rv.data + assert b'Invalid password' in rv.data def test_messages(self): """Test that messages work""" @@ -66,9 +66,9 @@ class FlaskrTestCase(unittest.TestCase): title='', text='HTML allowed here' ), follow_redirects=True) - assert 'No entries here so far' not in rv.data - assert '<Hello>' in rv.data - assert 'HTML allowed here' in rv.data + assert b'No entries here so far' not in rv.data + assert b'<Hello>' in rv.data + assert b'HTML allowed here' in rv.data if __name__ == '__main__': diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit.py index 2863de50..baa204f9 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit.py @@ -53,7 +53,7 @@ def init_db(): """Creates the database tables.""" with app.app_context(): db = get_db() - with app.open_resource('schema.sql') as f: + with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() diff --git a/examples/minitwit/minitwit_tests.py b/examples/minitwit/minitwit_tests.py index 87741165..c213466d 100644 --- a/examples/minitwit/minitwit_tests.py +++ b/examples/minitwit/minitwit_tests.py @@ -63,7 +63,7 @@ class MiniTwitTestCase(unittest.TestCase): rv = self.app.post('/add_message', data={'text': text}, follow_redirects=True) if text: - assert 'Your message was recorded' in rv.data + assert b'Your message was recorded' in rv.data return rv # testing functions @@ -71,29 +71,29 @@ class MiniTwitTestCase(unittest.TestCase): def test_register(self): """Make sure registering works""" rv = self.register('user1', 'default') - assert 'You were successfully registered ' \ - 'and can login now' in rv.data + assert b'You were successfully registered ' \ + b'and can login now' in rv.data rv = self.register('user1', 'default') - assert 'The username is already taken' in rv.data + assert b'The username is already taken' in rv.data rv = self.register('', 'default') - assert 'You have to enter a username' in rv.data + assert b'You have to enter a username' in rv.data rv = self.register('meh', '') - assert 'You have to enter a password' in rv.data + assert b'You have to enter a password' in rv.data rv = self.register('meh', 'x', 'y') - assert 'The two passwords do not match' in rv.data + assert b'The two passwords do not match' in rv.data rv = self.register('meh', 'foo', email='broken') - assert 'You have to enter a valid email address' in rv.data + assert b'You have to enter a valid email address' in rv.data def test_login_logout(self): """Make sure logging in and logging out works""" rv = self.register_and_login('user1', 'default') - assert 'You were logged in' in rv.data + assert b'You were logged in' in rv.data rv = self.logout() - assert 'You were logged out' in rv.data + assert b'You were logged out' in rv.data rv = self.login('user1', 'wrongpassword') - assert 'Invalid password' in rv.data + assert b'Invalid password' in rv.data rv = self.login('user2', 'wrongpassword') - assert 'Invalid username' in rv.data + assert b'Invalid username' in rv.data def test_message_recording(self): """Check if adding messages works""" @@ -101,8 +101,8 @@ class MiniTwitTestCase(unittest.TestCase): self.add_message('test message 1') self.add_message('') rv = self.app.get('/') - assert 'test message 1' in rv.data - assert '<test message 2>' in rv.data + assert b'test message 1' in rv.data + assert b'<test message 2>' in rv.data def test_timelines(self): """Make sure that timelines work""" @@ -112,37 +112,37 @@ class MiniTwitTestCase(unittest.TestCase): self.register_and_login('bar', 'default') self.add_message('the message by bar') rv = self.app.get('/public') - assert 'the message by foo' in rv.data - assert 'the message by bar' in rv.data + assert b'the message by foo' in rv.data + assert b'the message by bar' in rv.data # bar's timeline should just show bar's message rv = self.app.get('/') - assert 'the message by foo' not in rv.data - assert 'the message by bar' in rv.data + assert b'the message by foo' not in rv.data + assert b'the message by bar' in rv.data # now let's follow foo rv = self.app.get('/foo/follow', follow_redirects=True) - assert 'You are now following "foo"' in rv.data + assert b'You are now following "foo"' in rv.data # we should now see foo's message rv = self.app.get('/') - assert 'the message by foo' in rv.data - assert 'the message by bar' in rv.data + assert b'the message by foo' in rv.data + assert b'the message by bar' in rv.data # but on the user's page we only want the user's message rv = self.app.get('/bar') - assert 'the message by foo' not in rv.data - assert 'the message by bar' in rv.data + assert b'the message by foo' not in rv.data + assert b'the message by bar' in rv.data rv = self.app.get('/foo') - assert 'the message by foo' in rv.data - assert 'the message by bar' not in rv.data + assert b'the message by foo' in rv.data + assert b'the message by bar' not in rv.data # now unfollow and check if that worked rv = self.app.get('/foo/unfollow', follow_redirects=True) - assert 'You are no longer following "foo"' in rv.data + assert b'You are no longer following "foo"' in rv.data rv = self.app.get('/') - assert 'the message by foo' not in rv.data - assert 'the message by bar' in rv.data + assert b'the message by foo' not in rv.data + assert b'the message by bar' in rv.data if __name__ == '__main__': diff --git a/flask/app.py b/flask/app.py index dc684489..77e4799c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -630,6 +630,7 @@ class Flask(_PackageBoundObject): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. """ return open(os.path.join(self.instance_path, resource), mode) diff --git a/flask/helpers.py b/flask/helpers.py index dbbbf2e6..37d3bb49 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -838,6 +838,7 @@ class _PackageBoundObject(object): :param resource: the name of the resource. To access resources within subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. """ if mode not in ('r', 'rb'): raise ValueError('Resources can only be opened for reading') From af5576a6c50ec849a824c1ace15f187d378db771 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 19:46:26 +0200 Subject: [PATCH 1240/3747] fix iterator in testsuite helpers --- flask/testsuite/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 21c9f791..90e9e020 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -15,7 +15,7 @@ import unittest from logging import StreamHandler from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr from werkzeug.http import parse_cache_control_header, parse_options_header -from flask._compat import StringIO, text_type +from flask._compat import StringIO, text_type, implements_iterator def has_encoding(name): @@ -485,6 +485,7 @@ class StreamingTestCase(FlaskTestCase): app = flask.Flask(__name__) app.testing = True called = [] + @implements_iterator class Wrapper(object): def __init__(self, gen): self._gen = gen @@ -492,7 +493,7 @@ class StreamingTestCase(FlaskTestCase): return self def close(self): called.append(42) - def next(self): + def __next__(self): return next(self._gen) @app.route('/') def index(): From 83f76585725fd380b61f35576bb1c307fe2a1a5e Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 20:19:17 +0200 Subject: [PATCH 1241/3747] fix metaclass usage for py3 --- flask/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flask/views.py b/flask/views.py index 5192c1c1..b3b61b52 100644 --- a/flask/views.py +++ b/flask/views.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for more details. """ from .globals import request +from ._compat import with_metaclass http_method_funcs = frozenset(['get', 'post', 'head', 'options', @@ -119,7 +120,7 @@ class MethodViewType(type): return rv -class MethodView(View): +class MethodView(with_metaclass(MethodViewType, View)): """Like a regular class-based view but that dispatches requests to particular methods. For instance if you implement a method called :meth:`get` it means you will response to ``'GET'`` requests and @@ -138,8 +139,6 @@ class MethodView(View): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ - __metaclass__ = MethodViewType - def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) # if the request method is HEAD and we don't have a handler for it From 96b8ffbb29eaba834a30352554e42cf2406c7e06 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 20:24:14 +0200 Subject: [PATCH 1242/3747] always import from ._compat --- flask/app.py | 2 +- flask/config.py | 2 +- flask/debughelpers.py | 2 +- flask/exthook.py | 2 +- flask/helpers.py | 2 +- flask/testing.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flask/app.py b/flask/app.py index 77e4799c..84ef1013 100644 --- a/flask/app.py +++ b/flask/app.py @@ -34,7 +34,7 @@ from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ request_tearing_down, appcontext_tearing_down -from flask._compat import reraise, string_types, integer_types +from ._compat import reraise, string_types, integer_types # a lock used for logger initialization _logger_lock = Lock() diff --git a/flask/config.py b/flask/config.py index 4d9ac23a..155afa2f 100644 --- a/flask/config.py +++ b/flask/config.py @@ -14,7 +14,7 @@ import os import errno from werkzeug.utils import import_string -from flask._compat import string_types +from ._compat import string_types class ConfigAttribute(object): diff --git a/flask/debughelpers.py b/flask/debughelpers.py index f3bac185..2f8510f9 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -8,7 +8,7 @@ :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from flask._compat import implements_to_string +from ._compat import implements_to_string class UnexpectedUnicodeError(AssertionError, UnicodeError): diff --git a/flask/exthook.py b/flask/exthook.py index 89dac47b..d0d814c6 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -21,7 +21,7 @@ """ import sys import os -from flask._compat import reraise +from ._compat import reraise class ExtensionImporter(object): diff --git a/flask/helpers.py b/flask/helpers.py index 37d3bb49..49fd0278 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -24,7 +24,6 @@ from functools import update_wrapper from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound -from flask._compat import string_types, text_type # this was moved in 0.7 try: @@ -37,6 +36,7 @@ from jinja2 import FileSystemLoader from .signals import message_flashed from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request +from ._compat import string_types, text_type # sentinel diff --git a/flask/testing.py b/flask/testing.py index ef116cf3..4c1f4550 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -13,7 +13,7 @@ from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack -from flask._compat import urlparse +from ._compat import urlparse def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): From f9e9e774646ff7cbd2df6386c7055760936a9fcd Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 20:58:12 +0200 Subject: [PATCH 1243/3747] fix data types in after_request test TODO: why was that bug not causing / displaying an exception somehow? should give a TypeError in py 3.3. --- flask/testsuite/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 656233b8..9bbe10e3 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -443,7 +443,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): evts.append('before') @app.after_request def after_request(response): - response.data += '|after' + response.data += b'|after' evts.append('after') return response @app.route('/') @@ -453,7 +453,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): return 'request' self.assert_not_in('after', evts) rv = app.test_client().get('/').data - self.assert_in(b'after', evts) + self.assert_in('after', evts) self.assert_equal(rv, b'request|after') def test_after_request_processing(self): From 13cc69911c6b5c742489ffe6e8c6458dec32e230 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 25 May 2013 22:01:14 +0200 Subject: [PATCH 1244/3747] fix typos --- flask/_compat.py | 2 +- flask/app.py | 10 +++++----- flask/ctx.py | 2 +- flask/helpers.py | 2 +- flask/testsuite/blueprints.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flask/_compat.py b/flask/_compat.py index 27f61137..7c960ac6 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -91,7 +91,7 @@ else: def with_metaclass(meta, *bases): # This requires a bit of explanation: the basic idea is to make a - # dummy metaclass for one level of class instanciation that replaces + # dummy metaclass for one level of class instantiation that replaces # itself with the actual metaclass. Because of internal type checks # we also need to make sure that we downgrade the custom metaclass # for one level to something closer to type (that's why __call__ and diff --git a/flask/app.py b/flask/app.py index 84ef1013..c3bbc907 100644 --- a/flask/app.py +++ b/flask/app.py @@ -177,7 +177,7 @@ class Flask(_PackageBoundObject): #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled - #: exception ocurrs and the integrated server will automatically reload + #: exception occurs and the integrated server will automatically reload #: the application if changes in the code are detected. #: #: This attribute can also be configured from the config with the `DEBUG` @@ -522,7 +522,7 @@ class Flask(_PackageBoundObject): """The name of the application. This is usually the import name with the difference that it's guessed from the run file if the import name is main. This name is used as a display name when - Flask needs the name of the application. It can be set and overriden + Flask needs the name of the application. It can be set and overridden to change the value. .. versionadded:: 0.8 @@ -697,7 +697,7 @@ class Flask(_PackageBoundObject): This injects request, session, config and g into the template context as well as everything template context processors want to inject. Note that the as of Flask 0.6, the original values - in the context will not be overriden if a context processor + in the context will not be overridden if a context processor decides to return a value with the same key. :param context: the context as a dictionary that is updated in place @@ -1045,7 +1045,7 @@ class Flask(_PackageBoundObject): app.error_handler_spec[None][404] = page_not_found Setting error handlers via assignments to :attr:`error_handler_spec` - however is discouraged as it requires fidling with nested dictionaries + however is discouraged as it requires fiddling with nested dictionaries and the special case for arbitrary exception types. The first `None` refers to the active blueprint. If the error @@ -1550,7 +1550,7 @@ class Flask(_PackageBoundObject): # When we create a response object directly, we let the constructor # set the headers and status. We do this because there can be # some extra logic involved when creating these objects with - # specific values (like defualt content type selection). + # specific values (like default content type selection). if isinstance(rv, string_types): rv = self.response_class(rv, headers=headers, status=status) headers = status = None diff --git a/flask/ctx.py b/flask/ctx.py index 320f2f0e..6ab5b1ff 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -286,7 +286,7 @@ class RequestContext(object): def push(self): """Binds the request context to the current context.""" - # If an exception ocurrs in debug mode or if context preservation is + # If an exception occurs in debug mode or if context preservation is # activated under exception situations exactly one context stays # on the stack. The rationale is that you want to access that # information under debug situations. However if someone forgets to diff --git a/flask/helpers.py b/flask/helpers.py index 49fd0278..02b66797 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -330,7 +330,7 @@ def get_template_attribute(template_name, attribute): .. versionadded:: 0.2 :param template_name: the name of the template - :param attribute: the name of the variable of macro to acccess + :param attribute: the name of the variable of macro to access """ return getattr(current_app.jinja_env.get_template(template_name).module, attribute) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 97a196d7..b3771fde 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -105,7 +105,7 @@ class ModuleTestCase(FlaskTestCase): app = flask.Flask(__name__) admin = flask.Module(__name__, 'admin', url_prefix='/admin') @app.context_processor - def inject_all_regualr(): + def inject_all_regular(): return {'a': 1} @admin.context_processor def inject_admin(): @@ -534,7 +534,7 @@ class BlueprintTestCase(FlaskTestCase): c = app.test_client() self.assertEqual(c.get('/py/foo').data, b'bp.foo') - # The rule's din't actually made it through + # The rule's didn't actually made it through rv = c.get('/py/bar') assert rv.status_code == 404 rv = c.get('/py/bar/123') From bb2e20f53fd66981190658a58e206a3f8aa4f3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sun, 26 May 2013 15:37:52 +0200 Subject: [PATCH 1245/3747] Depends on itsdangerous>=0.12 now --- setup.py | 2 +- tox.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ddc83251..a0a65cf4 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ setup( install_requires=[ 'Werkzeug>=0.7', 'Jinja2>=2.4', - 'itsdangerous>=0.17' + 'itsdangerous>=0.21' ], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tox.ini b/tox.ini index c699ac48..161e86fb 100644 --- a/tox.ini +++ b/tox.ini @@ -3,5 +3,4 @@ envlist = py26, py27, pypy, py33 [testenv] deps = -egit+git://github.com/mitsuhiko/werkzeug.git@sprint-branch#egg=werkzeug - -egit+git://github.com/mitsuhiko/itsdangerous.git#egg=itsdangerous commands = python run-tests.py [] From ac04bc78361d5562f8289e8efe16a2e9a97b0d01 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sun, 26 May 2013 20:33:22 +0200 Subject: [PATCH 1246/3747] replace 1/0 by 1 // 0 to get rid of DeprecationWarning (and PEP8 issue) --- flask/testsuite/basic.py | 6 +++--- flask/testsuite/helpers.py | 10 +++++----- flask/testsuite/signals.py | 2 +- flask/testsuite/subclassing.py | 2 +- flask/testsuite/testing.py | 2 +- flask/testsuite/views.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 9bbe10e3..a172ed85 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -529,7 +529,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): pass @app.route('/') def fails(): - 1/0 + 1 // 0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) self.assert_in(b'Internal Server Error', rv.data) @@ -866,7 +866,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): app = flask.Flask(__name__) @app.route('/') def index(): - 1/0 + 1 // 0 c = app.test_client() if config_key is not None: app.config[config_key] = True @@ -1054,7 +1054,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): @app.route('/fail') def fail_func(): - 1/0 + 1 // 0 c = app.test_client() for x in range(3): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 90e9e020..2b04f73a 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -306,7 +306,7 @@ class LoggingTestCase(FlaskTestCase): @app.route('/exc') def exc(): - 1/0 + 1 // 0 with app.test_client() as c: with catch_stderr() as err: @@ -340,7 +340,7 @@ class LoggingTestCase(FlaskTestCase): @app.route('/') def index(): - 1/0 + 1 // 0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) @@ -349,7 +349,7 @@ class LoggingTestCase(FlaskTestCase): err = out.getvalue() self.assert_in('Exception on / [GET]', err) self.assert_in('Traceback (most recent call last):', err) - self.assert_in('1/0', err) + self.assert_in('1 // 0', err) self.assert_in('ZeroDivisionError:', err) def test_processor_exceptions(self): @@ -357,11 +357,11 @@ class LoggingTestCase(FlaskTestCase): @app.before_request def before_request(): if trigger == 'before': - 1/0 + 1 // 0 @app.after_request def after_request(response): if trigger == 'after': - 1/0 + 1 // 0 return response @app.route('/') def index(): diff --git a/flask/testsuite/signals.py b/flask/testsuite/signals.py index ffd575c1..94262f17 100644 --- a/flask/testsuite/signals.py +++ b/flask/testsuite/signals.py @@ -83,7 +83,7 @@ class SignalsTestCase(FlaskTestCase): @app.route('/') def index(): - 1/0 + 1 // 0 def record(sender, exception): recorded.append(exception) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index dbfdd499..6b81db98 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -30,7 +30,7 @@ class FlaskSubclassingTestCase(FlaskTestCase): @app.route('/') def index(): - 1/0 + 1 // 0 rv = app.test_client().get('/') self.assert_equal(rv.status_code, 500) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index cd96b497..a618f0b8 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -143,7 +143,7 @@ class TestToolsTestCase(FlaskTestCase): @app.route('/other') def other(): - 1/0 + 1 // 0 with app.test_client() as c: resp = c.get('/') diff --git a/flask/testsuite/views.py b/flask/testsuite/views.py index 9dd463f2..4eee015b 100644 --- a/flask/testsuite/views.py +++ b/flask/testsuite/views.py @@ -55,9 +55,9 @@ class ViewTestCase(FlaskTestCase): class Index(flask.views.MethodView): def get(self): - 1/0 + 1 // 0 def post(self): - 1/0 + 1 // 0 class Other(Index): def get(self): From 404265110ab52ffd2a5d4991f115913aacba94a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sun, 26 May 2013 21:46:22 +0200 Subject: [PATCH 1247/3747] Always return a list from get_flashed_messages --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 02b66797..3dade16c 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -398,7 +398,7 @@ def get_flashed_messages(with_categories=False, category_filter=[]): _request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \ if '_flashes' in session else [] if category_filter: - flashes = filter(lambda f: f[0] in category_filter, flashes) + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: return [x[1] for x in flashes] return flashes From 775c76ac5c4365deb31061d0a8ef942d0585f81d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 12:48:04 +0100 Subject: [PATCH 1248/3747] Enabled test mode for an app --- flask/testsuite/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 2b04f73a..bba66bda 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -133,6 +133,7 @@ class JSONTestCase(FlaskTestCase): class ModifiedRequest(flask.Request): url_charset = 'euc-kr' app = flask.Flask(__name__) + app.testing = True app.request_class = ModifiedRequest app.url_map.charset = 'euc-kr' From ffd9296507db6bfad1339b75c0ccdc7fa030cb67 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 12:51:12 +0100 Subject: [PATCH 1249/3747] Close request objects if they support closing. --- flask/ctx.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask/ctx.py b/flask/ctx.py index 6ab5b1ff..259c5e2f 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -334,6 +334,9 @@ class RequestContext(object): if exc is None: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) + request_close = getattr(self.request, 'close', None) + if request_close is not None: + request_close() clear_request = True rv = _request_ctx_stack.pop() From 47572c5b4006c54f0991587d5c07d599d0ad3325 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 14:24:29 +0100 Subject: [PATCH 1250/3747] Set the content length automatically before calling wrap_file --- flask/helpers.py | 1 + flask/testsuite/basic.py | 1 + 2 files changed, 2 insertions(+) diff --git a/flask/helpers.py b/flask/helpers.py index 3dade16c..f52b5ae4 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -516,6 +516,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if file is None: file = open(filename, 'rb') mtime = os.path.getmtime(filename) + headers['Content-Length'] = os.path.getsize(filename) data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index a172ed85..810b5e66 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -786,6 +786,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): def test_static_files(self): app = flask.Flask(__name__) + app.testing = True rv = app.test_client().get('/static/index.html') self.assert_equal(rv.status_code, 200) self.assert_equal(rv.data.strip(), b'

    Hello World!

    ') From eb622fb34f0b2433b21b6b5454273a597b77a6d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 14:31:36 +0100 Subject: [PATCH 1251/3747] Fixed a whole bunch of resource warnings in the flask testsuite --- flask/testsuite/basic.py | 1 + flask/testsuite/blueprints.py | 5 +++++ flask/testsuite/helpers.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 810b5e66..8cd3a822 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -793,6 +793,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): with app.test_request_context(): self.assert_equal(flask.url_for('static', filename='index.html'), '/static/index.html') + rv.close() def test_none_response(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index b3771fde..5935a473 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -172,8 +172,10 @@ class ModuleTestCase(FlaskTestCase): self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') self.assert_equal(rv.data.strip(), b'Admin File') + rv.close() rv = c.get('/admin/static/css/test.css') self.assert_equal(rv.data.strip(), b'/* nested file */') + rv.close() with app.test_request_context(): self.assert_equal(flask.url_for('admin.static', filename='test.txt'), @@ -354,8 +356,10 @@ class BlueprintTestCase(FlaskTestCase): self.assert_equal(rv.data, b'Hello from the Admin') rv = c.get('/admin/static/test.txt') self.assert_equal(rv.data.strip(), b'Admin File') + rv.close() rv = c.get('/admin/static/css/test.css') self.assert_equal(rv.data.strip(), b'/* nested file */') + rv.close() # try/finally, in case other tests use this app for Blueprint tests. max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT'] @@ -405,6 +409,7 @@ class BlueprintTestCase(FlaskTestCase): rv = blueprint.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 100) + rv.close() finally: app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index bba66bda..b74ad6bb 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -159,6 +159,7 @@ class SendfileTestCase(FlaskTestCase): self.assert_equal(rv.mimetype, 'text/html') with app.open_resource('static/index.html') as f: self.assert_equal(rv.data, f.read()) + rv.close() def test_send_file_xsendfile(self): app = flask.Flask(__name__) @@ -170,6 +171,7 @@ class SendfileTestCase(FlaskTestCase): self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) self.assert_equal(rv.mimetype, 'text/html') + rv.close() def test_send_file_object(self): app = flask.Flask(__name__) @@ -180,6 +182,7 @@ class SendfileTestCase(FlaskTestCase): with app.open_resource('static/index.html') as f: self.assert_equal(rv.data, f.read()) self.assert_equal(rv.mimetype, 'text/html') + rv.close() # mimetypes + etag self.assert_equal(len(captured), 2) @@ -192,6 +195,7 @@ class SendfileTestCase(FlaskTestCase): self.assert_in('x-sendfile', rv.headers) self.assert_equal(rv.headers['x-sendfile'], os.path.join(app.root_path, 'static/index.html')) + rv.close() # mimetypes + etag self.assert_equal(len(captured), 2) @@ -202,6 +206,7 @@ class SendfileTestCase(FlaskTestCase): rv = flask.send_file(f) self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'application/octet-stream') + rv.close() # etags self.assert_equal(len(captured), 1) with catch_warnings() as captured: @@ -209,6 +214,7 @@ class SendfileTestCase(FlaskTestCase): rv = flask.send_file(f, mimetype='text/plain') self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'text/plain') + rv.close() # etags self.assert_equal(len(captured), 1) @@ -218,6 +224,7 @@ class SendfileTestCase(FlaskTestCase): f = StringIO('Test') rv = flask.send_file(f) self.assert_not_in('x-sendfile', rv.headers) + rv.close() # etags self.assert_equal(len(captured), 1) @@ -229,6 +236,7 @@ class SendfileTestCase(FlaskTestCase): rv = flask.send_file(f, as_attachment=True) value, options = parse_options_header(rv.headers['Content-Disposition']) self.assert_equal(value, 'attachment') + rv.close() # mimetypes + etag self.assert_equal(len(captured), 2) @@ -238,6 +246,7 @@ class SendfileTestCase(FlaskTestCase): value, options = parse_options_header(rv.headers['Content-Disposition']) self.assert_equal(value, 'attachment') self.assert_equal(options['filename'], 'index.html') + rv.close() with app.test_request_context(): rv = flask.send_file(StringIO('Test'), as_attachment=True, @@ -247,6 +256,7 @@ class SendfileTestCase(FlaskTestCase): value, options = parse_options_header(rv.headers['Content-Disposition']) self.assert_equal(value, 'attachment') self.assert_equal(options['filename'], 'index.txt') + rv.close() def test_static_file(self): app = flask.Flask(__name__) @@ -256,20 +266,24 @@ class SendfileTestCase(FlaskTestCase): rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 12 * 60 * 60) + rv.close() # Test again with direct use of send_file utility. rv = flask.send_file('static/index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 12 * 60 * 60) + rv.close() app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600 with app.test_request_context(): # Test with static file handler. rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 3600) + rv.close() # Test again with direct use of send_file utility. rv = flask.send_file('static/index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 3600) + rv.close() class StaticFileApp(flask.Flask): def get_send_file_max_age(self, filename): return 10 @@ -279,10 +293,12 @@ class SendfileTestCase(FlaskTestCase): rv = app.send_static_file('index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 10) + rv.close() # Test again with direct use of send_file utility. rv = flask.send_file('static/index.html') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, 10) + rv.close() class LoggingTestCase(FlaskTestCase): From 8aaf3025864acbb803b652495d282bc5aa7a8128 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 14:35:23 +0100 Subject: [PATCH 1252/3747] Disable direct passthrough for accessing the data attribute on newer Werkzeugs --- flask/testsuite/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index b74ad6bb..20c15262 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -158,6 +158,7 @@ class SendfileTestCase(FlaskTestCase): self.assert_true(rv.direct_passthrough) self.assert_equal(rv.mimetype, 'text/html') with app.open_resource('static/index.html') as f: + rv.direct_passthrough = False self.assert_equal(rv.data, f.read()) rv.close() @@ -179,6 +180,7 @@ class SendfileTestCase(FlaskTestCase): with app.test_request_context(): f = open(os.path.join(app.root_path, 'static/index.html')) rv = flask.send_file(f) + rv.direct_passthrough = False with app.open_resource('static/index.html') as f: self.assert_equal(rv.data, f.read()) self.assert_equal(rv.mimetype, 'text/html') From 51042f4c9f2c68a63ad5b8ee3000a52518a4b87b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 30 May 2013 16:00:43 +0200 Subject: [PATCH 1253/3747] fix issues in test_context_refcounts that were unnoticed yet as they did not make the test fail --- flask/testsuite/appctx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/testsuite/appctx.py b/flask/testsuite/appctx.py index 54b9014e..8524d22b 100644 --- a/flask/testsuite/appctx.py +++ b/flask/testsuite/appctx.py @@ -87,8 +87,9 @@ class AppContextTestCase(FlaskTestCase): with flask._app_ctx_stack.top: with flask._request_ctx_stack.top: pass - self.assert_true(flask._request_ctx_stack.request.environ + self.assert_true(flask._request_ctx_stack.top.request.environ ['werkzeug.request'] is not None) + return u'' c = app.test_client() c.get('/') self.assertEqual(called, ['request', 'app']) From bbfef4c406506d89c662038c11c6c67bc97b67e3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 30 May 2013 16:02:28 +0200 Subject: [PATCH 1254/3747] flask view function may return bytes/str/unicode --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index c3bbc907..bfd615b7 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1551,7 +1551,7 @@ class Flask(_PackageBoundObject): # set the headers and status. We do this because there can be # some extra logic involved when creating these objects with # specific values (like default content type selection). - if isinstance(rv, string_types): + if isinstance(rv, string_types + (bytes, )): rv = self.response_class(rv, headers=headers, status=status) headers = status = None else: From abc1505196ba3bc9517ec142ced7c1204c3ac21b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 15:07:18 +0100 Subject: [PATCH 1255/3747] Fixed various issues on the Python 3 port --- flask/_compat.py | 4 ---- flask/json.py | 34 ++++++++++++++++++++++++++++++---- flask/testing.py | 8 ++++++-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/flask/_compat.py b/flask/_compat.py index 7c960ac6..edc9957a 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -47,8 +47,6 @@ if not PY2: encode_filename = _identity get_next = lambda x: x.__next__ - from urllib.parse import urlparse - else: unichr = unichr text_type = unicode @@ -86,8 +84,6 @@ else: return filename.encode('utf-8') return filename - from urlparse import urlparse - def with_metaclass(meta, *bases): # This requires a bit of explanation: the basic idea is to make a diff --git a/flask/json.py b/flask/json.py index 22f6067d..9e56073c 100644 --- a/flask/json.py +++ b/flask/json.py @@ -8,10 +8,11 @@ :copyright: (c) 2012 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import io import uuid from datetime import datetime from .globals import current_app, request -from ._compat import text_type +from ._compat import text_type, PY2 from werkzeug.http import http_date @@ -33,6 +34,20 @@ __all__ = ['dump', 'dumps', 'load', 'loads', 'htmlsafe_dump', 'jsonify'] +def _wrap_reader_for_text(fp, encoding): + if isinstance(fp.read(0), bytes): + fp = io.TextIOWrapper(io.BufferedReader(fp), encoding) + return fp + + +def _wrap_writer_for_text(fp, encoding): + try: + fp.write('') + except TypeError: + fp = io.TextIOWrapper(fp, encoding) + return fp + + class JSONEncoder(_json.JSONEncoder): """The default Flask JSON encoder. This one extends the default simplejson encoder by also supporting ``datetime`` objects, ``UUID`` as well as @@ -100,13 +115,20 @@ def dumps(obj, **kwargs): and can be overriden by the simplejson ``ensure_ascii`` parameter. """ _dump_arg_defaults(kwargs) - return _json.dumps(obj, **kwargs) + encoding = kwargs.pop('encoding', None) + rv = _json.dumps(obj, **kwargs) + if encoding is not None and isinstance(rv, text_type): + rv = rv.encode(encoding) + return rv def dump(obj, fp, **kwargs): """Like :func:`dumps` but writes into a file object.""" _dump_arg_defaults(kwargs) - return _json.dump(obj, fp, **kwargs) + encoding = kwargs.pop('encoding', None) + if encoding is not None: + fp = _wrap_writer_for_text(fp, encoding) + _json.dump(obj, fp, **kwargs) def loads(s, **kwargs): @@ -115,6 +137,8 @@ def loads(s, **kwargs): application on the stack. """ _load_arg_defaults(kwargs) + if isinstance(s, bytes): + s = s.decode(kwargs.pop('encoding', None) or 'utf-8') return _json.loads(s, **kwargs) @@ -122,6 +146,8 @@ def load(fp, **kwargs): """Like :func:`loads` but reads from a file object. """ _load_arg_defaults(kwargs) + if not PY2: + fp = _wrap_reader_for_text(fp, kwargs.pop('encoding', None) or 'utf-8') return _json.load(fp, **kwargs) @@ -148,7 +174,7 @@ def jsonify(*args, **kwargs): to this function are the same as to the :class:`dict` constructor. Example usage:: - + from flask import jsonify @app.route('/_get_current_user') diff --git a/flask/testing.py b/flask/testing.py index 4c1f4550..1dc383af 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -13,7 +13,11 @@ from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack -from ._compat import urlparse + +try: + from werkzeug.urls import url_parse +except ImportError: + from urlparse import urlsplit as url_parse def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): @@ -21,7 +25,7 @@ def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): http_host = app.config.get('SERVER_NAME') app_root = app.config.get('APPLICATION_ROOT') if base_url is None: - url = urlparse(path) + url = url_parse(path) base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') if app_root: base_url += app_root.lstrip('/') From 9ae8487330e0dc3a8d169d89cadf85fb11ded1ea Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 16:16:39 +0100 Subject: [PATCH 1256/3747] Fixed a broekn testcase --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index f52b5ae4..06343cf3 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -516,7 +516,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if file is None: file = open(filename, 'rb') mtime = os.path.getmtime(filename) - headers['Content-Length'] = os.path.getsize(filename) + headers['Content-Length'] = os.path.getsize(filename) data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, From 90e3906d02780c47e813649b1e282bfd279d7cb1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 17:58:27 +0100 Subject: [PATCH 1257/3747] Fixed some test failures --- flask/testsuite/basic.py | 6 ++++-- flask/testsuite/blueprints.py | 1 + flask/testsuite/helpers.py | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 8cd3a822..bd5b2760 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -513,7 +513,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): # test that all teardown_requests get passed the same original # exception. try: - raise TypeError + raise TypeError() except: pass @app.teardown_request @@ -524,7 +524,7 @@ class BasicFunctionalityTestCase(FlaskTestCase): # test that all teardown_requests get passed the same original # exception. try: - raise TypeError + raise TypeError() except: pass @app.route('/') @@ -1098,7 +1098,9 @@ class SubdomainTestCase(FlaskTestCase): app.register_module(mod) c = app.test_client() rv = c.get('/static/hello.txt', 'http://foo.example.com/') + rv.direct_passthrough = False self.assert_equal(rv.data.strip(), b'Hello Subdomain') + rv.close() def test_subdomain_matching(self): app = flask.Flask(__name__) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 5935a473..3414eeaa 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -371,6 +371,7 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/admin/static/css/test.css') cc = parse_cache_control_header(rv.headers['Cache-Control']) self.assert_equal(cc.max_age, expected_max_age) + rv.close() finally: app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 20c15262..7750ae52 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -206,6 +206,7 @@ class SendfileTestCase(FlaskTestCase): with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f) + rv.direct_passthrough = False self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'application/octet-stream') rv.close() @@ -214,6 +215,7 @@ class SendfileTestCase(FlaskTestCase): with catch_warnings() as captured: f = StringIO('Test') rv = flask.send_file(f, mimetype='text/plain') + rv.direct_passthrough = False self.assert_equal(rv.data, b'Test') self.assert_equal(rv.mimetype, 'text/plain') rv.close() From f1918093ac70d589a4d67af0d77140734c06c13d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 18:15:17 +0100 Subject: [PATCH 1258/3747] Changed teardown error handling to be more reliable. --- CHANGES | 3 +++ flask/app.py | 20 +++++++++++++++++++- flask/ctx.py | 13 ++++++++----- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index fbde1e2a..17290290 100644 --- a/CHANGES +++ b/CHANGES @@ -59,6 +59,9 @@ Release date to be decided. strongly discouraged as the interface was flawed. - Python requirements changed: requiring Python 2.6 or 2.7 now to prepare for Python 3.3 port. +- Changed how the teardown system is informed about exceptions. This is now + more reliable in case something handles an exception halfway through + the error handling process. Version 0.9 ----------- diff --git a/flask/app.py b/flask/app.py index bfd615b7..28a0a59a 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1510,6 +1510,16 @@ class Flask(_PackageBoundObject): rv.allow.update(methods) return rv + def should_ignore_error(self, error): + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns `True` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + def make_response(self, rv): """Converts the return value from a view function to a real response object that is an instance of :attr:`response_class`. @@ -1790,12 +1800,20 @@ class Flask(_PackageBoundObject): a list of headers and an optional exception context to start the response """ - with self.request_context(environ): + ctx = self.request_context(environ) + ctx.push() + error = None + try: try: response = self.full_dispatch_request() except Exception as e: + error = e response = self.make_response(self.handle_exception(e)) return response(environ, start_response) + finally: + if self.should_ignore_error(error): + error = None + ctx.auto_pop(error) @property def modules(self): diff --git a/flask/ctx.py b/flask/ctx.py index 259c5e2f..5e1ee2e3 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -352,6 +352,13 @@ class RequestContext(object): if app_ctx is not None: app_ctx.pop(exc) + def auto_pop(self, exc): + if self.request.environ.get('flask._preserve_context') or \ + (exc is not None and self.app.preserve_context_on_exception): + self.preserved = True + else: + self.pop(exc) + def __enter__(self): self.push() return self @@ -362,11 +369,7 @@ class RequestContext(object): # access the request object in the interactive shell. Furthermore # the context can be force kept alive for the test client. # See flask.testing for how this works. - if self.request.environ.get('flask._preserve_context') or \ - (tb is not None and self.app.preserve_context_on_exception): - self.preserved = True - else: - self.pop(exc_value) + self.auto_pop(exc_value) def __repr__(self): return '<%s \'%s\' [%s] of %s>' % ( From e07dcb5562c336975e31ce014aa33d2ab1f5ac98 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 18:17:04 +0100 Subject: [PATCH 1259/3747] Adjusted a testcase for Python 3 --- flask/testsuite/ext.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flask/testsuite/ext.py b/flask/testsuite/ext.py index 1a7a4a5a..5cc3df43 100644 --- a/flask/testsuite/ext.py +++ b/flask/testsuite/ext.py @@ -121,7 +121,10 @@ class ExtImportHookTestCase(FlaskTestCase): self.assert_true(tb.tb_frame.f_globals is globals()) # reraise() adds a second frame so we need to skip that one too. + # On PY3 we even have another one :( next = tb.tb_next.tb_next + if not PY2: + next = next.tb_next self.assert_in('flask_broken/__init__.py', next.tb_frame.f_code.co_filename) From b8aa9fed9a6837d29c472c642539dcf3496479ab Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 18:19:01 +0100 Subject: [PATCH 1260/3747] Added tox-test command --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4b5e4fe2..b67c8a9f 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: clean-pyc ext-test test test-with-mem upload-docs docs audit +.PHONY: clean-pyc ext-test test tox-test test-with-mem upload-docs docs audit all: clean-pyc test test: python run-tests.py +tox-test: + tox + test-with-mem: RUN_FLASK_MEMORY_TESTS=1 python run-tests.py From 6bd5dfad0c24d1a1077e7b1e00ffd6d64ba07a31 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 30 May 2013 21:39:54 +0100 Subject: [PATCH 1261/3747] Test Flask against werkzeug master --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 161e86fb..a8782e00 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,5 @@ envlist = py26, py27, pypy, py33 [testenv] -deps = -egit+git://github.com/mitsuhiko/werkzeug.git@sprint-branch#egg=werkzeug +deps = -egit+git://github.com/mitsuhiko/werkzeug.git#egg=werkzeug commands = python run-tests.py [] From e9fa24cfa367754c4435224cb9840f0a84a5faec Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 31 May 2013 00:56:09 +0100 Subject: [PATCH 1262/3747] Make travis install development werkzeug --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8c9c592d..b0b18d1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,9 @@ python: - pypy - 3.3 -install: pip install --editable . +install: + - pip install git+git://github.com/mitsuhiko/werkzeug.git#egg=Werkzeug + - pip install --editable . script: make test From 1c8c21abd529249db567b701400cf764555941b5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 31 May 2013 00:59:54 +0100 Subject: [PATCH 1263/3747] Let travis notify the #pocoo irc channel --- .travis.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b0b18d1c..8b2607d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,11 @@ branches: - website notifications: - # Disable travis notifications until they figured out how to hide - # their own builder failure from us. Travis currently fails way - # too many times by itself. email: false + irc: + channels: + - "chat.freenode.net#pocoo" + on_success: change + on_failure: always + use_notice: true + skip_join: true From 3d9055b3b73b1d09f933bfd070f704e9e8bdff2a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 1 Jun 2013 00:20:00 +0100 Subject: [PATCH 1264/3747] Added the JSONIFY_PRETTYPRINT_REGULAR config variable. This fixes #725 --- CHANGES | 1 + docs/config.rst | 7 ++++++- flask/app.py | 3 ++- flask/json.py | 10 +++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 17290290..7be9c77a 100644 --- a/CHANGES +++ b/CHANGES @@ -62,6 +62,7 @@ Release date to be decided. - Changed how the teardown system is informed about exceptions. This is now more reliable in case something handles an exception halfway through the error handling process. +- Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. Version 0.9 ----------- diff --git a/docs/config.rst b/docs/config.rst index 134fe36d..0dbb71d7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -149,6 +149,11 @@ The following configuration values are used internally by Flask: unicode strings. ``jsonfiy`` will automatically encode it in ``utf-8`` then for transport for instance. +``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` (the default) + jsonify responses will be pretty printed + if they are not requested by an + XMLHttpRequest object (controlled by + the ``X-Requested-With`` header) ================================= ========================================= .. admonition:: More on ``SERVER_NAME`` @@ -192,7 +197,7 @@ The following configuration values are used internally by Flask: ``PREFERRED_URL_SCHEME`` .. versionadded:: 0.10 - ``JSON_AS_ASCII`` + ``JSON_AS_ASCII``, ``JSONIFY_PRETTYPRINT_REGULAR`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 5bd3d1de..86f2a212 100644 --- a/flask/app.py +++ b/flask/app.py @@ -290,7 +290,8 @@ class Flask(_PackageBoundObject): 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False, 'PREFERRED_URL_SCHEME': 'http', - 'JSON_AS_ASCII': True + 'JSON_AS_ASCII': True, + 'JSONIFY_PRETTYPRINT_REGULAR': True, }) #: The rule object to use for URL rules created. This is used by diff --git a/flask/json.py b/flask/json.py index 9e56073c..e43c4ed9 100644 --- a/flask/json.py +++ b/flask/json.py @@ -194,8 +194,16 @@ def jsonify(*args, **kwargs): For security reasons only objects are supported toplevel. For more information about this, have a look at :ref:`json-security`. + This function's response will be pretty printed if it was not requested + with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless + the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false. + .. versionadded:: 0.2 """ + indent = None + if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] \ + and not request.is_xhr: + indent = 2 return current_app.response_class(dumps(dict(*args, **kwargs), - indent=None if request.is_xhr else 2), + indent=indent), mimetype='application/json') From 77d293cf49e586f03fbea96d0bae237bc7ed230f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 1 Jun 2013 19:24:03 +0100 Subject: [PATCH 1265/3747] Order JSON keys by default to avoid trashing HTTP caches --- CHANGES | 2 ++ docs/config.rst | 13 ++++++++++++- flask/app.py | 1 + flask/json.py | 2 ++ flask/testsuite/helpers.py | 40 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7be9c77a..378488f6 100644 --- a/CHANGES +++ b/CHANGES @@ -63,6 +63,8 @@ Release date to be decided. more reliable in case something handles an exception halfway through the error handling process. - Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. +- Flask now orders JSON keys by default to not trash HTTP caches due to + different hash seeds between different workers. Version 0.9 ----------- diff --git a/docs/config.rst b/docs/config.rst index 0dbb71d7..ced2ad82 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -149,6 +149,17 @@ The following configuration values are used internally by Flask: unicode strings. ``jsonfiy`` will automatically encode it in ``utf-8`` then for transport for instance. +``JSON_SORT_KEYS`` By default Flask will serialize JSON + objects in a way that the keys are + ordered. This is done in order to + ensure that independent of the hash seed + of the dictionary the return value will + be consistent to not trash external HTTP + caches. You can override the default + behavior by changing this variable. + This is not recommended but might give + you a performance improvement on the + cost of cachability. ``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` (the default) jsonify responses will be pretty printed if they are not requested by an @@ -197,7 +208,7 @@ The following configuration values are used internally by Flask: ``PREFERRED_URL_SCHEME`` .. versionadded:: 0.10 - ``JSON_AS_ASCII``, ``JSONIFY_PRETTYPRINT_REGULAR`` + ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` Configuring from Files ---------------------- diff --git a/flask/app.py b/flask/app.py index 86f2a212..7b286571 100644 --- a/flask/app.py +++ b/flask/app.py @@ -291,6 +291,7 @@ class Flask(_PackageBoundObject): 'TRAP_HTTP_EXCEPTIONS': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, + 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': True, }) diff --git a/flask/json.py b/flask/json.py index e43c4ed9..875af67e 100644 --- a/flask/json.py +++ b/flask/json.py @@ -92,10 +92,12 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" + kwargs.setdefault('sort_keys', True) if current_app: kwargs.setdefault('cls', current_app.json_encoder) if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) + kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) def _load_arg_defaults(kwargs): diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 7750ae52..c88c6241 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -148,6 +148,46 @@ class JSONTestCase(FlaskTestCase): if not has_encoding('euc-kr'): test_modified_url_encoding = None + def test_json_key_sorting(self): + app = flask.Flask(__name__) + app.testing = True + self.assert_equal(app.config['JSON_SORT_KEYS'], True) + d = dict.fromkeys(range(20), 'foo') + + @app.route('/') + def index(): + return flask.jsonify(values=d) + + c = app.test_client() + rv = c.get('/') + lines = [x.strip() for x in rv.data.strip().decode('utf-8').splitlines()] + self.assert_equal(lines, [ + '{', + '"values": {', + '"0": "foo",', + '"1": "foo",', + '"2": "foo",', + '"3": "foo",', + '"4": "foo",', + '"5": "foo",', + '"6": "foo",', + '"7": "foo",', + '"8": "foo",', + '"9": "foo",', + '"10": "foo",', + '"11": "foo",', + '"12": "foo",', + '"13": "foo",', + '"14": "foo",', + '"15": "foo",', + '"16": "foo",', + '"17": "foo",', + '"18": "foo",', + '"19": "foo"', + '}', + '}' + ]) + class SendfileTestCase(FlaskTestCase): From c629f69e698fd815969e91e5c8c59323c61a256c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 11:54:22 +0100 Subject: [PATCH 1266/3747] Make the JSON module work better in the absence of an application context --- flask/json.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask/json.py b/flask/json.py index 875af67e..2437a20c 100644 --- a/flask/json.py +++ b/flask/json.py @@ -92,18 +92,22 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" - kwargs.setdefault('sort_keys', True) if current_app: kwargs.setdefault('cls', current_app.json_encoder) if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) + else: + kwargs.setdefault('sort_keys', True) + kwargs.setdefault('cls', JSONEncoder) def _load_arg_defaults(kwargs): """Inject default arguments for load functions.""" if current_app: kwargs.setdefault('cls', current_app.json_decoder) + else: + kwargs.setdefault('cls', JSONDecoder) def dumps(obj, **kwargs): From 0190b770a1e3339c2cd96e6a44f44083aeeebb54 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 17:23:53 +0100 Subject: [PATCH 1267/3747] Removed a bunch of code from _compat --- flask/_compat.py | 42 ++------------------------------------ flask/helpers.py | 5 ++++- flask/testsuite/helpers.py | 4 ++-- 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/flask/_compat.py b/flask/_compat.py index edc9957a..1d4e9808 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -13,13 +13,10 @@ import sys PY2 = sys.version_info[0] == 2 -PYPY = hasattr(sys, 'pypy_translation_info') _identity = lambda x: x if not PY2: - unichr = chr - range_type = range text_type = str string_types = (str,) integer_types = (int, ) @@ -28,29 +25,17 @@ if not PY2: itervalues = lambda d: iter(d.values()) iteritems = lambda d: iter(d.items()) - import pickle - from io import BytesIO, StringIO - NativeStringIO = StringIO + from io import StringIO def reraise(tp, value, tb=None): if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value - ifilter = filter - imap = map - izip = zip - intern = sys.intern - - implements_iterator = _identity implements_to_string = _identity - encode_filename = _identity - get_next = lambda x: x.__next__ else: - unichr = unichr text_type = unicode - range_type = xrange string_types = (str, unicode) integer_types = (int, long) @@ -58,32 +43,15 @@ else: itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() - import cPickle as pickle - from cStringIO import StringIO as BytesIO, StringIO - NativeStringIO = BytesIO + from cStringIO import StringIO as StringIO exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') - from itertools import imap, izip, ifilter - intern = intern - - def implements_iterator(cls): - cls.next = cls.__next__ - del cls.__next__ - return cls - def implements_to_string(cls): cls.__unicode__ = cls.__str__ cls.__str__ = lambda x: x.__unicode__().encode('utf-8') return cls - get_next = lambda x: x.next - - def encode_filename(filename): - if isinstance(filename, unicode): - return filename.encode('utf-8') - return filename - def with_metaclass(meta, *bases): # This requires a bit of explanation: the basic idea is to make a @@ -103,9 +71,3 @@ def with_metaclass(meta, *bases): return type.__new__(cls, name, (), d) return meta(name, bases, d) return metaclass('temporary_class', None, {}) - - -try: - from urllib.parse import quote_from_bytes as url_quote -except ImportError: - from urllib import quote as url_quote diff --git a/flask/helpers.py b/flask/helpers.py index 06343cf3..1e7c87f0 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -18,9 +18,12 @@ from time import time from zlib import adler32 from threading import RLock from werkzeug.routing import BuildError -from werkzeug.urls import url_quote from functools import update_wrapper +try: + from werkzeug.urls import url_quote +except ImportError: + from urlparse import quote as url_quote from werkzeug.datastructures import Headers from werkzeug.exceptions import NotFound diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index c88c6241..b02025a9 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -15,7 +15,7 @@ import unittest from logging import StreamHandler from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr from werkzeug.http import parse_cache_control_header, parse_options_header -from flask._compat import StringIO, text_type, implements_iterator +from flask._compat import StringIO, text_type def has_encoding(name): @@ -546,7 +546,6 @@ class StreamingTestCase(FlaskTestCase): app = flask.Flask(__name__) app.testing = True called = [] - @implements_iterator class Wrapper(object): def __init__(self, gen): self._gen = gen @@ -556,6 +555,7 @@ class StreamingTestCase(FlaskTestCase): called.append(42) def __next__(self): return next(self._gen) + next = __next__ @app.route('/') def index(): def generate(): From 6dfe9332606a497a1931f9cad4a150f2866e3cda Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 17:25:04 +0100 Subject: [PATCH 1268/3747] Removed an unnecessary as statement --- flask/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/_compat.py b/flask/_compat.py index 1d4e9808..c3428845 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -43,7 +43,7 @@ else: itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() - from cStringIO import StringIO as StringIO + from cStringIO import StringIO exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') From 1b40b3b573f5d98cf9fbc453305cd535c0b2578d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 21:47:28 +0100 Subject: [PATCH 1269/3747] Fixed request context preservation and teardown handler interaction. --- CHANGES | 3 +++ flask/app.py | 7 +++++++ flask/ctx.py | 16 +++++++++++++++- flask/testsuite/basic.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 378488f6..5bd76b97 100644 --- a/CHANGES +++ b/CHANGES @@ -62,6 +62,9 @@ Release date to be decided. - Changed how the teardown system is informed about exceptions. This is now more reliable in case something handles an exception halfway through the error handling process. +- Request context preservation in debug mode now keeps the exception + information around which means that teardown handlers are able to + distinguish error from success cases. - Added the ``JSONIFY_PRETTYPRINT_REGULAR`` configuration variable. - Flask now orders JSON keys by default to not trash HTTP caches due to different hash seeds between different workers. diff --git a/flask/app.py b/flask/app.py index 7b286571..b52af9b2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1707,6 +1707,13 @@ class Flask(_PackageBoundObject): rv = func(exc) request_tearing_down.send(self, exc=exc) + # If this interpreter supports clearing the exception information + # we do that now. This will only go into effect on Python 2.x, + # on 3.x it disappears automatically at the end of the exception + # stack. + if hasattr(sys, 'exc_clear'): + sys.exc_clear() + def do_teardown_appcontext(self, exc=None): """Called when an application context is popped. This works pretty much the same as :meth:`do_teardown_request` but for the application diff --git a/flask/ctx.py b/flask/ctx.py index 5e1ee2e3..6ea3158f 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -235,6 +235,10 @@ class RequestContext(object): # is pushed the preserved context is popped. self.preserved = False + # remembers the exception for pop if there is one in case the context + # preservation kicks in. + self._preserved_exc = None + # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. @@ -296,7 +300,7 @@ class RequestContext(object): # functionality is not active in production environments. top = _request_ctx_stack.top if top is not None and top.preserved: - top.pop() + top.pop(top._preserved_exc) # Before we push the request context we have to ensure that there # is an application context. @@ -331,9 +335,18 @@ class RequestContext(object): clear_request = False if not self._implicit_app_ctx_stack: self.preserved = False + self._preserved_exc = None if exc is None: exc = sys.exc_info()[1] self.app.do_teardown_request(exc) + + # If this interpreter supports clearing the exception information + # we do that now. This will only go into effect on Python 2.x, + # on 3.x it disappears automatically at the end of the exception + # stack. + if hasattr(sys, 'exc_clear'): + sys.exc_clear() + request_close = getattr(self.request, 'close', None) if request_close is not None: request_close() @@ -356,6 +369,7 @@ class RequestContext(object): if self.request.environ.get('flask._preserve_context') or \ (exc is not None and self.app.preserve_context_on_exception): self.preserved = True + self._preserved_exc = exc else: self.pop(exc) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index bd5b2760..50ffcef8 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -1070,6 +1070,40 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_true(flask._request_ctx_stack.top is None) self.assert_true(flask._app_ctx_stack.top is None) + def test_preserve_remembers_exception(self): + app = flask.Flask(__name__) + app.debug = True + errors = [] + + @app.route('/fail') + def fail_func(): + 1 // 0 + + @app.route('/success') + def success_func(): + return 'Okay' + + @app.teardown_request + def teardown_handler(exc): + errors.append(exc) + + c = app.test_client() + + # After this failure we did not yet call the teardown handler + with self.assert_raises(ZeroDivisionError): + c.get('/fail') + self.assert_equal(errors, []) + + # But this request triggers it, and it's an error + c.get('/success') + self.assert_equal(len(errors), 2) + self.assert_true(isinstance(errors[0], ZeroDivisionError)) + + # At this point another request does nothing. + c.get('/success') + self.assert_equal(len(errors), 3) + self.assert_equal(errors[1], None) + class SubdomainTestCase(FlaskTestCase): From 56d3b74488346a03b74fb4a9fd633cc5f79191d8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 2 Jun 2013 23:24:28 +0100 Subject: [PATCH 1270/3747] Added a test for non-ascii routing --- flask/testsuite/basic.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 50ffcef8..3a60eb81 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -963,6 +963,18 @@ class BasicFunctionalityTestCase(FlaskTestCase): expected = '/login' self.assert_equal(url, expected) + def test_nonascii_pathinfo(self): + app = flask.Flask(__name__) + app.testing = True + + @app.route(u'/киртест') + def index(): + return 'Hello World!' + + c = app.test_client() + rv = c.get(u'/киртест') + self.assert_equal(rv.data, b'Hello World!') + def test_debug_mode_complains_after_first_request(self): app = flask.Flask(__name__) app.debug = True From ef72b78042d7feffc864e7f2da3f62835fc63ee8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 3 Jun 2013 12:25:08 +0100 Subject: [PATCH 1271/3747] Imply the |safe on tojson in templates and change escaping logic --- CHANGES | 2 ++ docs/api.rst | 5 ++--- docs/patterns/jquery.rst | 11 ++++++++--- docs/templating.rst | 6 ++---- flask/app.py | 9 +-------- flask/json.py | 27 ++++++++++++++++++++------- flask/testsuite/helpers.py | 17 +++++++++++------ 7 files changed, 46 insertions(+), 31 deletions(-) diff --git a/CHANGES b/CHANGES index 5bd76b97..ff0a3e83 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,8 @@ Release date to be decided. ``template_filter`` method family. - Set the content-length header for x-sendfile. - ``tojson`` filter now does not escape script blocks in HTML5 parsers. +- ``tojson`` used in templates is now safe by default due. This was + allowed due to the different escaping behavior. - Flask will now raise an error if you attempt to register a new function on an already used endpoint. - Added wrapper module around simplejson and added default serialization diff --git a/docs/api.rst b/docs/api.rst index 27333079..096741ae 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -364,7 +364,8 @@ JSON module: The :func:`~htmlsafe_dumps` function of this json module is also available as filter called ``|tojson`` in Jinja2. Note that inside `script` tags no escaping must take place, so make sure to disable escaping -with ``|safe`` if you intend to use it inside `script` tags: +with ``|safe`` if you intend to use it inside `script` tags unless +you are using Flask 0.10 which implies that: .. sourcecode:: html+jinja @@ -372,8 +373,6 @@ with ``|safe`` if you intend to use it inside `script` tags: doSomethingWith({{ user.username|tojson|safe }}); -Note that the ``|tojson`` filter escapes forward slashes properly. - .. autofunction:: jsonify .. autofunction:: dumps diff --git a/docs/patterns/jquery.rst b/docs/patterns/jquery.rst index 7aaa2803..9de99f61 100644 --- a/docs/patterns/jquery.rst +++ b/docs/patterns/jquery.rst @@ -63,9 +63,10 @@ like this: $SCRIPT_ROOT = {{ request.script_root|tojson|safe }}; -The ``|safe`` is necessary so that Jinja does not escape the JSON encoded -string with HTML rules. Usually this would be necessary, but we are -inside a `script` block here where different rules apply. +The ``|safe`` is necessary in Flask before 0.10 so that Jinja does not +escape the JSON encoded string with HTML rules. Usually this would be +necessary, but we are inside a `script` block here where different rules +apply. .. admonition:: Information for Pros @@ -76,6 +77,10 @@ inside a `script` block here where different rules apply. escape slashes for you (``{{ ""|tojson|safe }}`` is rendered as ``"<\/script>"``). + In Flask 0.10 it goes a step further and escapes all HTML tags with + unicode escapes. This makes it possible for Flask to automatically + mark the result as HTML safe. + JSON View Functions ------------------- diff --git a/docs/templating.rst b/docs/templating.rst index b6e1fc0a..4e432333 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -106,8 +106,8 @@ by Jinja2 itself: fly. Note that inside `script` tags no escaping must take place, so make - sure to disable escaping with ``|safe`` if you intend to use it inside - `script` tags: + sure to disable escaping with ``|safe`` before Flask 0.10 if you intend + to use it inside `script` tags: .. sourcecode:: html+jinja @@ -115,8 +115,6 @@ by Jinja2 itself: doSomethingWith({{ user.username|tojson|safe }}); - That the ``|tojson`` filter escapes forward slashes properly for you. - Controlling Autoescaping ------------------------ diff --git a/flask/app.py b/flask/app.py index b52af9b2..271c29cd 100644 --- a/flask/app.py +++ b/flask/app.py @@ -659,7 +659,7 @@ class Flask(_PackageBoundObject): session=session, g=g ) - rv.filters['tojson'] = json.htmlsafe_dumps + rv.filters['tojson'] = json.tojson_filter return rv def create_global_jinja_loader(self): @@ -1707,13 +1707,6 @@ class Flask(_PackageBoundObject): rv = func(exc) request_tearing_down.send(self, exc=exc) - # If this interpreter supports clearing the exception information - # we do that now. This will only go into effect on Python 2.x, - # on 3.x it disappears automatically at the end of the exception - # stack. - if hasattr(sys, 'exc_clear'): - sys.exc_clear() - def do_teardown_appcontext(self, exc=None): """Called when an application context is popped. This works pretty much the same as :meth:`do_teardown_request` but for the application diff --git a/flask/json.py b/flask/json.py index 2437a20c..d1cda5ae 100644 --- a/flask/json.py +++ b/flask/json.py @@ -15,6 +15,7 @@ from .globals import current_app, request from ._compat import text_type, PY2 from werkzeug.http import http_date +from jinja2 import Markup # Use the same json implementation as itsdangerous on which we # depend anyways. @@ -160,18 +161,26 @@ def load(fp, **kwargs): def htmlsafe_dumps(obj, **kwargs): """Works exactly like :func:`dumps` but is safe for use in ``"|tojson|safe }}') - self.assert_equal(rv, '"<\\/script>"') - rv = render('{{ "<\0/script>"|tojson|safe }}') - self.assert_equal(rv, '"<\\u0000\\/script>"') - rv = render('{{ " + +

    {% endif %} {% endif %} -