Error handling invokes parent exceptions (Fixes #74)
This commit is contained in:
committed by
Miguel Grinberg
parent
4a9b92b800
commit
24d74fb848
@@ -314,7 +314,7 @@ automatically handled by Microdot are:
|
||||
While the above errors are fully complaint with the HTTP specification, the
|
||||
application might want to provide custom responses for them. The
|
||||
:func:`errorhandler() <microdot.Microdot.errorhandler>` decorator registers
|
||||
a functions to respond to specific error codes. The following example shows a
|
||||
functions to respond to specific error codes. The following example shows a
|
||||
custom error handler for 404 errors::
|
||||
|
||||
@app.errorhandler(404)
|
||||
@@ -322,14 +322,18 @@ custom error handler for 404 errors::
|
||||
return {'error': 'resource not found'}, 404
|
||||
|
||||
The ``errorhandler()`` decorator has a second form, in which it takes an
|
||||
exception class as an argument. Microdot will then invoke the handler when an
|
||||
exception of that class is raised. The next example provides a custom response
|
||||
for division by zero errors::
|
||||
exception class as an argument. Microdot will then invoke the handler when the
|
||||
exception is an instance of the given class is raised. The next example
|
||||
provides a custom response for division by zero errors::
|
||||
|
||||
@app.errorhandler(ZeroDivisionError)
|
||||
def division_by_zero(request, exception):
|
||||
return {'error': 'division by zero'}, 500
|
||||
|
||||
When the raised exception class does not have an error handler defined, but
|
||||
one or more of its base classes do, Microdot makes an attempt to invoke the
|
||||
most specific handler.
|
||||
|
||||
Mounting a Sub-Application
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -145,6 +145,39 @@ class NoCaseDict(dict):
|
||||
return super().get(self.keymap.get(kl, kl), default)
|
||||
|
||||
|
||||
def mro(cls): # pragma: no cover
|
||||
"""Return the method resolution order of a class.
|
||||
|
||||
This is a helper function that returns the method resolution order of a
|
||||
class. It is used by Microdot to find the best error handler to invoke for
|
||||
the raised exception.
|
||||
|
||||
In CPython, this function returns the ``__mro__`` attribute of the class.
|
||||
In MicroPython, this function implements a recursive depth-first scanning
|
||||
of the class hierarchy.
|
||||
"""
|
||||
if hasattr(cls, 'mro'):
|
||||
return cls.__mro__
|
||||
|
||||
def _mro(cls):
|
||||
m = [cls]
|
||||
for base in cls.__bases__:
|
||||
m += _mro(base)
|
||||
return m
|
||||
|
||||
mro_list = _mro(cls)
|
||||
|
||||
# If a class appears multiple times (due to multiple inheritance) remove
|
||||
# all but the last occurence. This matches the method resolution order
|
||||
# of MicroPython, but not CPython.
|
||||
mro_pruned = []
|
||||
for i in range(len(mro_list)):
|
||||
base = mro_list.pop(0)
|
||||
if base not in mro_list:
|
||||
mro_pruned.append(base)
|
||||
return mro_pruned
|
||||
|
||||
|
||||
class MultiDict(dict):
|
||||
"""A subclass of dictionary that can hold multiple values for the same
|
||||
key. It is used to hold key/value pairs decoded from query strings and
|
||||
@@ -1104,10 +1137,18 @@ class Microdot():
|
||||
res = exc.reason, exc.status_code
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
exc_class = None
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
exc_class = exc.__class__
|
||||
else:
|
||||
for c in mro(exc.__class__)[1:]:
|
||||
if c in self.error_handlers:
|
||||
exc_class = c
|
||||
break
|
||||
if exc_class:
|
||||
try:
|
||||
res = self.error_handlers[exc.__class__](req, exc)
|
||||
res = self.error_handlers[exc_class](req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
|
||||
@@ -17,6 +17,7 @@ except ImportError:
|
||||
import io
|
||||
|
||||
from microdot import Microdot as BaseMicrodot
|
||||
from microdot import mro
|
||||
from microdot import NoCaseDict
|
||||
from microdot import Request as BaseRequest
|
||||
from microdot import Response as BaseResponse
|
||||
@@ -393,11 +394,19 @@ class Microdot(BaseMicrodot):
|
||||
res = exc.reason, exc.status_code
|
||||
except Exception as exc:
|
||||
print_exception(exc)
|
||||
exc_class = None
|
||||
res = None
|
||||
if exc.__class__ in self.error_handlers:
|
||||
exc_class = exc.__class__
|
||||
else:
|
||||
for c in mro(exc.__class__)[1:]:
|
||||
if c in self.error_handlers:
|
||||
exc_class = c
|
||||
break
|
||||
if exc_class:
|
||||
try:
|
||||
res = await self._invoke_handler(
|
||||
self.error_handlers[exc.__class__], req, exc)
|
||||
self.error_handlers[exc_class], req, exc)
|
||||
except Exception as exc2: # pragma: no cover
|
||||
print_exception(exc2)
|
||||
if res is None:
|
||||
|
||||
@@ -465,6 +465,90 @@ class TestMicrodot(unittest.TestCase):
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, '501')
|
||||
|
||||
def test_exception_handler_parent(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
def handle_lookup_error(req, exc):
|
||||
return exc.__class__.__name__, 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.get('/')
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'IndexError')
|
||||
|
||||
def test_exception_handler_redundant_parent(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
def handle_lookup_error(req, exc):
|
||||
return 'LookupError', 501
|
||||
|
||||
@app.errorhandler(IndexError)
|
||||
def handle_index_error(req, exc):
|
||||
return 'IndexError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.get('/')
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'IndexError')
|
||||
|
||||
def test_exception_handler_multiple_parents(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_generic_exception(req, exc):
|
||||
return 'Exception', 501
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
def handle_lookup_error(req, exc):
|
||||
return 'LookupError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.get('/')
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'LookupError')
|
||||
|
||||
def test_exception_handler_no_viable_parents(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(RuntimeError)
|
||||
def handle_runtime_error(req, exc):
|
||||
return 'RuntimeError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = client.get('/')
|
||||
self.assertEqual(res.status_code, 500)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'Internal server error')
|
||||
|
||||
def test_abort(self):
|
||||
app = Microdot()
|
||||
|
||||
|
||||
@@ -500,6 +500,90 @@ class TestMicrodotAsync(unittest.TestCase):
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, '501')
|
||||
|
||||
def test_exception_handler_parent(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
async def handle_lookup_error(req, exc):
|
||||
return exc.__class__.__name__, 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = self._run(client.get('/'))
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'IndexError')
|
||||
|
||||
def test_exception_handler_redundant_parent(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
async def handle_lookup_error(req, exc):
|
||||
return 'LookupError', 501
|
||||
|
||||
@app.errorhandler(IndexError)
|
||||
async def handle_index_error(req, exc):
|
||||
return 'IndexError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = self._run(client.get('/'))
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'IndexError')
|
||||
|
||||
def test_exception_handler_multiple_parents(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
async def handle_generic_exception(req, exc):
|
||||
return 'Exception', 501
|
||||
|
||||
@app.errorhandler(LookupError)
|
||||
async def handle_lookup_error(req, exc):
|
||||
return 'LookupError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = self._run(client.get('/'))
|
||||
self.assertEqual(res.status_code, 501)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'LookupError')
|
||||
|
||||
def test_exception_handler_no_viable_parents(self):
|
||||
app = Microdot()
|
||||
|
||||
@app.route('/')
|
||||
def index(req):
|
||||
foo = []
|
||||
return foo[1]
|
||||
|
||||
@app.errorhandler(RuntimeError)
|
||||
async def handle_runtime_error(req, exc):
|
||||
return 'RuntimeError', 501
|
||||
|
||||
client = TestClient(app)
|
||||
res = self._run(client.get('/'))
|
||||
self.assertEqual(res.status_code, 500)
|
||||
self.assertEqual(res.headers['Content-Type'],
|
||||
'text/plain; charset=UTF-8')
|
||||
self.assertEqual(res.text, 'Internal server error')
|
||||
|
||||
def test_abort(self):
|
||||
app = Microdot()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user