commit 33850c0ebd23ae615e6823993d441f46d80b1ff0 Author: Armin Ronacher Date: Tue Apr 6 13:12:57 2010 +0200 Initial checkin of stuff that exists so far. 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)