Error handling invokes parent exceptions (Fixes #74)

This commit is contained in:
Diego Pomares
2022-10-15 20:30:04 +02:00
committed by Miguel Grinberg
parent 4a9b92b800
commit 24d74fb848
5 changed files with 231 additions and 9 deletions

View File

@@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -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:

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()