Browse files

Added blueprint specific error handling

  • Loading branch information...
1 parent aaa24fc commit f5ec9952decda8731a1f40ba788ba06d12c0229b @mitsuhiko mitsuhiko committed Jun 5, 2011
Showing with 182 additions and 12 deletions.
  1. +5 −0 CHANGES
  2. +94 −11 flask/app.py
  3. +23 −1 flask/blueprints.py
  4. +60 −0 tests/flask_tests.py
View
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
-------------
View
105 flask/app.py
@@ -234,12 +234,21 @@ def __init__(self, import_name, static_path=None, static_url_path=None,
#: 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 @@ def __init__(self, import_name, static_path=None, static_url_path=None,
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,29 +781,59 @@ def decorator(f):
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::
@app.errorhandler(404)
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 @@ def handle_http_exception(self, e):
.. 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 @@ def handle_exception(self, e):
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 @@ def full_dispatch_request(self):
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)
View
24 flask/blueprints.py
@@ -62,6 +62,7 @@ def __init__(self, name, import_name, static_folder=None,
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 @@ def add_url_rule(self, rule, endpoint=None, view_func=None, **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
- 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 @@ def app_url_defaults(self, f):
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
View
60 tests/flask_tests.py
@@ -531,6 +531,22 @@ def error():
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 @@ def index():
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))

0 comments on commit f5ec995

Please sign in to comment.