mirror of https://github.com/pallets/flask.git
Added blueprint specific error handling
This commit is contained in:
parent
aaa24fc05a
commit
f5ec9952de
5
CHANGES
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
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
Reference in New Issue