Browse Source

Added blueprint specific error handling

pull/262/head
Armin Ronacher 13 years ago
parent
commit
f5ec9952de
  1. 5
      CHANGES
  2. 105
      flask/app.py
  3. 24
      flask/blueprints.py
  4. 60
      tests/flask_tests.py

5
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
-------------

105
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)

24
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

60
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))

Loading…
Cancel
Save