Skip to content

Commit

Permalink
Merge branch 'master' into fix-issue-220
Browse files Browse the repository at this point in the history
Conflicts:
	flask/helpers.py
  • Loading branch information
jfinkels committed May 8, 2012
2 parents 1e2aae2 + 086348e commit 1492c48
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 28 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -70,6 +70,9 @@ 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.
- Added :func:`flask.after_this_request`.

Version 0.8.1
-------------
Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Expand Up @@ -289,6 +289,8 @@ Useful Functions and Classes

.. autofunction:: make_response

.. autofunction:: after_this_request

.. autofunction:: send_file

.. autofunction:: send_from_directory
Expand Down Expand Up @@ -641,6 +643,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():
Expand Down
3 changes: 2 additions & 1 deletion flask/__init__.py
Expand Up @@ -25,7 +25,8 @@
get_template_attribute, make_response, safe_join, JSONEncoder
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
Expand Down
54 changes: 32 additions & 22 deletions flask/app.py
Expand Up @@ -19,7 +19,7 @@
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

Expand Down Expand Up @@ -373,16 +373,14 @@ def __init__(self, import_name, static_path=None, static_url_path=None,
#: 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
Expand Down Expand Up @@ -949,6 +947,10 @@ def index():
# 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.
Expand All @@ -957,11 +959,14 @@ def index():

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
Expand Down Expand Up @@ -1522,19 +1527,24 @@ def inject_url_defaults(self, endpoint, values):
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
Expand Down Expand Up @@ -1577,7 +1587,7 @@ def process_response(self, response):
"""
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:
Expand Down
30 changes: 30 additions & 0 deletions flask/ctx.py
Expand Up @@ -11,6 +11,7 @@

import sys

from functools import partial
from werkzeug.exceptions import HTTPException

from .globals import _request_ctx_stack, _app_ctx_stack
Expand All @@ -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
Expand Down Expand Up @@ -153,6 +179,10 @@ def __init__(self, app, environ):
# 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
Expand Down
5 changes: 3 additions & 2 deletions flask/helpers.py
Expand Up @@ -12,7 +12,6 @@
from __future__ import with_statement

import datetime
import imp
import os
import sys
import pkgutil
Expand Down Expand Up @@ -307,10 +306,12 @@ def external_url_handler(error, 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)
Expand Down
21 changes: 18 additions & 3 deletions flask/testsuite/basic.py
Expand Up @@ -411,6 +411,21 @@ def index():
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__)
Expand Down Expand Up @@ -712,13 +727,13 @@ def test_build_error_handler(self):
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/')

Expand Down

0 comments on commit 1492c48

Please sign in to comment.