From f5ec9952decda8731a1f40ba788ba06d12c0229b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 5 Jun 2011 10:27:15 +0200 Subject: [PATCH] Added blueprint specific error handling --- CHANGES | 5 +++ flask/app.py | 105 ++++++++++++++++++++++++++++++++++++++----- flask/blueprints.py | 24 +++++++++- tests/flask_tests.py | 60 +++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 12 deletions(-) diff --git a/CHANGES b/CHANGES index 92a147b0..3a367ead 100644 --- a/CHANGES +++ b/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 ------------- diff --git a/flask/app.py b/flask/app.py index e8d2672f..8f371dba 100644 --- a/flask/app.py +++ b/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) diff --git a/flask/blueprints.py b/flask/blueprints.py index b804c316..1fed639d 100644 --- a/flask/blueprints.py +++ b/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 diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 2abf1336..7c430417 100644 --- a/tests/flask_tests.py +++ b/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))