Permalink
Browse files

Merge branch 'master' into fix-issue-220

Conflicts:
	flask/helpers.py
  • Loading branch information...
2 parents 1e2aae2 + 086348e commit 1492c483b0fdda33180481f4f1a5d3cba851a50a @jfinkels committed May 8, 2012
Showing with 94 additions and 28 deletions.
  1. +3 −0 CHANGES
  2. +6 −0 docs/api.rst
  3. +2 −1 flask/__init__.py
  4. +32 −22 flask/app.py
  5. +30 −0 flask/ctx.py
  6. +3 −2 flask/helpers.py
  7. +18 −3 flask/testsuite/basic.py
View
3 CHANGES
@@ -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
-------------
View
6 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
@@ -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():
View
3 flask/__init__.py
@@ -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
View
54 flask/app.py
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -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:
View
30 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 @@ 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
View
5 flask/helpers.py
@@ -12,7 +12,6 @@
from __future__ import with_statement
import datetime
-import imp
import os
import sys
import pkgutil
@@ -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)
View
21 flask/testsuite/basic.py
@@ -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__)
@@ -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/')

0 comments on commit 1492c48

Please sign in to comment.