From eae48d97b085626c1860a900a27ff63c439e0edb Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 23 Dec 2014 19:08:14 +0100 Subject: [PATCH 1/8] Fixed and intuitivized exception handling --- flask/app.py | 124 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/flask/app.py b/flask/app.py index 1f7df2e8..b6e0ca1f 100644 --- a/flask/app.py +++ b/flask/app.py @@ -8,6 +8,7 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from collections import ChainMap import os import sys @@ -19,7 +20,7 @@ from functools import update_wrapper from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule, RequestRedirect, BuildError from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed, BadRequest + MethodNotAllowed, BadRequest, default_exceptions from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ locked_cached_property, _endpoint_from_view_func, find_package @@ -65,6 +66,64 @@ def setupmethod(f): return update_wrapper(wrapper_func, f) +def get_http_code(error_class_or_instance): + if ( + isinstance(error_class_or_instance, HTTPException) or + isinstance(error_class_or_instance, type) and + issubclass(error_class_or_instance, HTTPException) + ): + return error_class_or_instance.code + return None + + +class ExceptionHandlerDict(ChainMap): + """A dict storing exception handlers or falling back to the default ones + + Designed to be app.error_handler_spec[blueprint_or_none] + And hold a Exception → handler function mapping. + Converts error codes to default HTTPException subclasses. + + Returns None if no handler is defined for blueprint or app + """ + def __init__(self, app, blueprint): + self.app = app + init = super(ExceptionHandlerDict, self).__init__ + if blueprint: # fall back to app mapping + init({}, app.error_handler_spec[None]) + else: + init({}) + + def get_class(self, exc_class_or_code): + if isinstance(exc_class_or_code, integer_types): + # ensure that we register only exceptions as keys + exc_class = default_exceptions[exc_class_or_code] + else: + assert issubclass(exc_class_or_code, Exception) + exc_class = exc_class_or_code + return exc_class + + def __contains__(self, e_or_c): + return super(ExceptionHandlerDict, self).__contains__(self.get_class(e_or_c)) + + def __getitem__(self, e_or_c): + return super(ExceptionHandlerDict, self).__getitem__(self.get_class(e_or_c)) + + def __setitem__(self, e_or_c, handler): + assert callable(handler) + return super(ExceptionHandlerDict, self).__setitem__(self.get_class(e_or_c), handler) + + def find_handler(self, ex_instance): + assert isinstance(ex_instance, Exception) + + for superclass in type(ex_instance).mro(): + if superclass is BaseException: + return None + handler = self.get(superclass) + if handler is not None: + return handler + return None + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -365,7 +424,7 @@ class Flask(_PackageBoundObject): # support for the now deprecated `error_handlers` attribute. The # :attr:`error_handler_spec` shall be used now. - self._error_handlers = {} + self._error_handlers = ExceptionHandlerDict(self, None) #: A dictionary of all registered error handlers. The key is ``None`` #: for error handlers active on the application, otherwise the key is @@ -1136,16 +1195,30 @@ class Flask(_PackageBoundObject): @setupmethod 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, integer_types): - assert code_or_exception != 500 or key is None, \ + """ + :type key: None|str + :type code_or_exception: int|T<=Exception + :type f: callable + """ + assert not isinstance(code_or_exception, HTTPException) # old broken behavior + + code = code_or_exception + is_code = isinstance(code_or_exception, integer_types) + if not is_code: + if issubclass(code_or_exception, HTTPException): + code = code_or_exception.code + else: + code = None + + handlers = self.error_handler_spec.setdefault(key, ExceptionHandlerDict(self, key)) + + if is_code: + # TODO: why is this? + assert code != 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)) + + handlers[code_or_exception] = f @setupmethod def template_filter(self, name=None): @@ -1386,6 +1459,13 @@ class Flask(_PackageBoundObject): self.url_default_functions.setdefault(None, []).append(f) return f + def _find_error_handler(self, e): + """Finds a registered error handler for the request’s blueprint. + If nether blueprint nor App has a suitable handler registered, returns None + """ + handlers = self.error_handler_spec.get(request.blueprint, self.error_handler_spec[None]) + return handlers.find_handler(e) + def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the @@ -1393,15 +1473,12 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.3 """ - handlers = self.error_handler_spec.get(request.blueprint) # Proxy exceptions don't have error codes. We want to always return # those unchanged as errors if e.code is None: return e - if handlers and e.code in handlers: - handler = handlers[e.code] - else: - handler = self.error_handler_spec[None].get(e.code) + + handler = self._find_error_handler(e) if handler is None: return e return handler(e) @@ -1443,20 +1520,15 @@ class Flask(_PackageBoundObject): # wants the traceback preserved in handle_http_exception. Of course # we cannot prevent users from trashing it themselves in a custom # trap_http_exception method so that's their fault then. - - 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) - + if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) - reraise(exc_type, exc_value, tb) + handler = self._find_error_handler(e) + + if handler is None: + reraise(exc_type, exc_value, tb) + return handler(e) def handle_exception(self, e): """Default exception handling that kicks in when an exception From 7126a22334c881101b5c5f8bdc00d008cb7229aa Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Tue, 23 Dec 2014 20:51:44 +0100 Subject: [PATCH 2/8] Switched to userdict for obsolete Python versions --- flask/app.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/flask/app.py b/flask/app.py index b6e0ca1f..00f65807 100644 --- a/flask/app.py +++ b/flask/app.py @@ -8,14 +8,13 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -from collections import ChainMap - import os import sys from threading import Lock from datetime import timedelta from itertools import chain from functools import update_wrapper +from collections import Mapping from werkzeug.datastructures import ImmutableDict from werkzeug.routing import Map, Rule, RequestRedirect, BuildError @@ -34,7 +33,7 @@ from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ request_tearing_down, appcontext_tearing_down -from ._compat import reraise, string_types, text_type, integer_types +from ._compat import reraise, string_types, text_type, integer_types, iterkeys # a lock used for logger initialization _logger_lock = Lock() @@ -76,7 +75,7 @@ def get_http_code(error_class_or_instance): return None -class ExceptionHandlerDict(ChainMap): +class ExceptionHandlerDict(Mapping): """A dict storing exception handlers or falling back to the default ones Designed to be app.error_handler_spec[blueprint_or_none] @@ -86,12 +85,13 @@ class ExceptionHandlerDict(ChainMap): Returns None if no handler is defined for blueprint or app """ def __init__(self, app, blueprint): + super(ExceptionHandlerDict, self).__init__() self.app = app - init = super(ExceptionHandlerDict, self).__init__ + self.data = {} if blueprint: # fall back to app mapping - init({}, app.error_handler_spec[None]) + self.fallback = app.error_handler_spec[None] else: - init({}) + self.fallback = {} def get_class(self, exc_class_or_code): if isinstance(exc_class_or_code, integer_types): @@ -103,14 +103,28 @@ class ExceptionHandlerDict(ChainMap): return exc_class def __contains__(self, e_or_c): - return super(ExceptionHandlerDict, self).__contains__(self.get_class(e_or_c)) + clazz = self.get_class(e_or_c) + return clazz in self.data or clazz in self.fallback def __getitem__(self, e_or_c): - return super(ExceptionHandlerDict, self).__getitem__(self.get_class(e_or_c)) + clazz = self.get_class(e_or_c) + item = self.data.get(clazz) + if item is not None: + return item + elif len(self.fallback): + return self.fallback[clazz] + else: + raise KeyError(e_or_c) def __setitem__(self, e_or_c, handler): assert callable(handler) - return super(ExceptionHandlerDict, self).__setitem__(self.get_class(e_or_c), handler) + self.data[self.get_class(e_or_c)] = handler + + def __iter__(self): + return iterkeys(self.data) + + def __len__(self): + return len(self.data) def find_handler(self, ex_instance): assert isinstance(ex_instance, Exception) From a6c6cc18f5eab148cb15a65fe6543fb5f1272765 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Thu, 25 Dec 2014 16:11:11 +0100 Subject: [PATCH 3/8] Removed unused http code get function --- flask/app.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/flask/app.py b/flask/app.py index 00f65807..81861399 100644 --- a/flask/app.py +++ b/flask/app.py @@ -65,16 +65,6 @@ def setupmethod(f): return update_wrapper(wrapper_func, f) -def get_http_code(error_class_or_instance): - if ( - isinstance(error_class_or_instance, HTTPException) or - isinstance(error_class_or_instance, type) and - issubclass(error_class_or_instance, HTTPException) - ): - return error_class_or_instance.code - return None - - class ExceptionHandlerDict(Mapping): """A dict storing exception handlers or falling back to the default ones @@ -93,7 +83,8 @@ class ExceptionHandlerDict(Mapping): else: self.fallback = {} - def get_class(self, exc_class_or_code): + @staticmethod + def get_class(exc_class_or_code): if isinstance(exc_class_or_code, integer_types): # ensure that we register only exceptions as keys exc_class = default_exceptions[exc_class_or_code] From aa4700c276999a002bd6ddf8c7d35993429ec2d7 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Sun, 28 Dec 2014 16:07:56 +0100 Subject: [PATCH 4/8] More verbose message for old broken behavior --- flask/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 81861399..e1266d7e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1205,7 +1205,11 @@ class Flask(_PackageBoundObject): :type code_or_exception: int|T<=Exception :type f: callable """ - assert not isinstance(code_or_exception, HTTPException) # old broken behavior + if isinstance(code_or_exception, HTTPException): # old broken behavior + raise ValueError( + 'Tried to register a handler for an exception instance {0!r}. ' + 'Handlers can only be registered for exception classes or HTTP error codes.' + .format(code_or_exception)) code = code_or_exception is_code = isinstance(code_or_exception, integer_types) From 0e44cca8e32f17e78f73a127ebe8d8c627c3fe13 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Sun, 28 Dec 2014 16:13:16 +0100 Subject: [PATCH 5/8] Removed 500 not being registerable for blueprints --- flask/app.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/flask/app.py b/flask/app.py index e1266d7e..e784eea2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1211,22 +1211,8 @@ class Flask(_PackageBoundObject): 'Handlers can only be registered for exception classes or HTTP error codes.' .format(code_or_exception)) - code = code_or_exception - is_code = isinstance(code_or_exception, integer_types) - if not is_code: - if issubclass(code_or_exception, HTTPException): - code = code_or_exception.code - else: - code = None - handlers = self.error_handler_spec.setdefault(key, ExceptionHandlerDict(self, key)) - if is_code: - # TODO: why is this? - assert code != 500 or key is None, \ - 'It is currently not possible to register a 500 internal ' \ - 'server error on a per-blueprint level.' - handlers[code_or_exception] = f @setupmethod @@ -1551,7 +1537,7 @@ class Flask(_PackageBoundObject): exc_type, exc_value, tb = sys.exc_info() got_request_exception.send(self, exception=e) - handler = self.error_handler_spec[None].get(500) + handler = self._find_error_handler(InternalServerError()) if self.propagate_exceptions: # if we want to repropagate the exception, we can attempt to From fd8e6b26f9ff59bc8bccd6bd251bee5838551ba8 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Sun, 28 Dec 2014 17:19:09 +0100 Subject: [PATCH 6/8] removed ExceptionHandlerDict --- flask/app.py | 113 ++++++++++++------------------- tests/test_user_error_handler.py | 113 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 tests/test_user_error_handler.py diff --git a/flask/app.py b/flask/app.py index e784eea2..6fc95c2c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -65,70 +65,6 @@ def setupmethod(f): return update_wrapper(wrapper_func, f) -class ExceptionHandlerDict(Mapping): - """A dict storing exception handlers or falling back to the default ones - - Designed to be app.error_handler_spec[blueprint_or_none] - And hold a Exception → handler function mapping. - Converts error codes to default HTTPException subclasses. - - Returns None if no handler is defined for blueprint or app - """ - def __init__(self, app, blueprint): - super(ExceptionHandlerDict, self).__init__() - self.app = app - self.data = {} - if blueprint: # fall back to app mapping - self.fallback = app.error_handler_spec[None] - else: - self.fallback = {} - - @staticmethod - def get_class(exc_class_or_code): - if isinstance(exc_class_or_code, integer_types): - # ensure that we register only exceptions as keys - exc_class = default_exceptions[exc_class_or_code] - else: - assert issubclass(exc_class_or_code, Exception) - exc_class = exc_class_or_code - return exc_class - - def __contains__(self, e_or_c): - clazz = self.get_class(e_or_c) - return clazz in self.data or clazz in self.fallback - - def __getitem__(self, e_or_c): - clazz = self.get_class(e_or_c) - item = self.data.get(clazz) - if item is not None: - return item - elif len(self.fallback): - return self.fallback[clazz] - else: - raise KeyError(e_or_c) - - def __setitem__(self, e_or_c, handler): - assert callable(handler) - self.data[self.get_class(e_or_c)] = handler - - def __iter__(self): - return iterkeys(self.data) - - def __len__(self): - return len(self.data) - - def find_handler(self, ex_instance): - assert isinstance(ex_instance, Exception) - - for superclass in type(ex_instance).mro(): - if superclass is BaseException: - return None - handler = self.get(superclass) - if handler is not None: - return handler - return None - - class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -429,7 +365,7 @@ class Flask(_PackageBoundObject): # support for the now deprecated `error_handlers` attribute. The # :attr:`error_handler_spec` shall be used now. - self._error_handlers = ExceptionHandlerDict(self, None) + 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 @@ -1142,6 +1078,23 @@ class Flask(_PackageBoundObject): return f return decorator + @staticmethod + def _ensure_exc_class(exc_class_or_code): + """ensure that we register only exceptions as handler keys""" + if isinstance(exc_class_or_code, integer_types): + exc_class = default_exceptions[exc_class_or_code] + elif isinstance(exc_class_or_code, type): + exc_class = exc_class_or_code + else: + exc_class = type(exc_class_or_code) + + assert issubclass(exc_class, Exception) + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + @setupmethod def errorhandler(self, code_or_exception): """A decorator that is used to register a function give a given @@ -1211,9 +1164,10 @@ class Flask(_PackageBoundObject): 'Handlers can only be registered for exception classes or HTTP error codes.' .format(code_or_exception)) - handlers = self.error_handler_spec.setdefault(key, ExceptionHandlerDict(self, key)) + exc_class, code = self._ensure_exc_class(code_or_exception) - handlers[code_or_exception] = f + handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) + handlers[exc_class] = f @setupmethod def template_filter(self, name=None): @@ -1456,10 +1410,29 @@ class Flask(_PackageBoundObject): def _find_error_handler(self, e): """Finds a registered error handler for the request’s blueprint. - If nether blueprint nor App has a suitable handler registered, returns None + If neither blueprint nor App has a suitable handler registered, returns None """ - handlers = self.error_handler_spec.get(request.blueprint, self.error_handler_spec[None]) - return handlers.find_handler(e) + exc_class, code = self._ensure_exc_class(e) + + def find_superclass(d): + if not d: + return None + for superclass in exc_class.mro(): + if superclass is BaseException: + return None + handler = d.get(superclass) + if handler is not None: + return handler + return None + + # try blueprint handlers + handler = find_superclass(self.error_handler_spec.get(request.blueprint, {}).get(code)) + + if handler is not None: + return handler + + # fall back to app handlers + return find_superclass(self.error_handler_spec[None].get(code)) def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py new file mode 100644 index 00000000..78f4de3c --- /dev/null +++ b/tests/test_user_error_handler.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from werkzeug.exceptions import Forbidden, InternalServerError +import flask + + +def test_error_handler_subclass(): + app = flask.Flask(__name__) + + class ParentException(Exception): + pass + + class ChildExceptionUnregistered(ParentException): + pass + + class ChildExceptionRegistered(ParentException): + pass + + @app.errorhandler(ParentException) + def parent_exception_handler(e): + assert isinstance(e, ParentException) + return 'parent' + + @app.errorhandler(ChildExceptionRegistered) + def child_exception_handler(e): + assert isinstance(e, ChildExceptionRegistered) + return 'child-registered' + + @app.route('/parent') + def parent_test(): + raise ParentException() + + @app.route('/child-unregistered') + def unregistered_test(): + raise ChildExceptionUnregistered() + + @app.route('/child-registered') + def registered_test(): + raise ChildExceptionRegistered() + + + c = app.test_client() + + assert c.get('/parent').data == b'parent' + assert c.get('/child-unregistered').data == b'parent' + assert c.get('/child-registered').data == b'child-registered' + + +def test_error_handler_http_subclass(): + app = flask.Flask(__name__) + + class ForbiddenSubclassRegistered(Forbidden): + pass + + class ForbiddenSubclassUnregistered(Forbidden): + pass + + @app.errorhandler(403) + def code_exception_handler(e): + assert isinstance(e, Forbidden) + return 'forbidden' + + @app.errorhandler(ForbiddenSubclassRegistered) + def subclass_exception_handler(e): + assert isinstance(e, ForbiddenSubclassRegistered) + return 'forbidden-registered' + + @app.route('/forbidden') + def forbidden_test(): + raise Forbidden() + + @app.route('/forbidden-registered') + def registered_test(): + raise ForbiddenSubclassRegistered() + + @app.route('/forbidden-unregistered') + def unregistered_test(): + raise ForbiddenSubclassUnregistered() + + + c = app.test_client() + + assert c.get('/forbidden').data == b'forbidden' + assert c.get('/forbidden-unregistered').data == b'forbidden' + assert c.get('/forbidden-registered').data == b'forbidden-registered' + + +def test_error_handler_blueprint(): + bp = flask.Blueprint('bp', __name__) + + @bp.errorhandler(500) + def bp_exception_handler(e): + return 'bp-error' + + @bp.route('/error') + def bp_test(): + raise InternalServerError() + + app = flask.Flask(__name__) + + @app.errorhandler(500) + def app_exception_handler(e): + return 'app-error' + + @app.route('/error') + def app_test(): + raise InternalServerError() + + app.register_blueprint(bp, url_prefix='/bp') + + c = app.test_client() + + assert c.get('/error').data == b'app-error' + assert c.get('/bp/error').data == b'bp-error' \ No newline at end of file From 8c054f04b87dc7d713495d91fdb4d52bdef0e643 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Sat, 3 Jan 2015 01:31:38 +0100 Subject: [PATCH 7/8] added caching --- flask/app.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/flask/app.py b/flask/app.py index 6fc95c2c..9c6d5064 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1079,14 +1079,12 @@ class Flask(_PackageBoundObject): return decorator @staticmethod - def _ensure_exc_class(exc_class_or_code): + def _get_exc_class_and_code(exc_class_or_code): """ensure that we register only exceptions as handler keys""" if isinstance(exc_class_or_code, integer_types): exc_class = default_exceptions[exc_class_or_code] - elif isinstance(exc_class_or_code, type): - exc_class = exc_class_or_code else: - exc_class = type(exc_class_or_code) + exc_class = exc_class_or_code assert issubclass(exc_class, Exception) @@ -1164,7 +1162,7 @@ class Flask(_PackageBoundObject): 'Handlers can only be registered for exception classes or HTTP error codes.' .format(code_or_exception)) - exc_class, code = self._ensure_exc_class(code_or_exception) + exc_class, code = self._get_exc_class_and_code(code_or_exception) handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) handlers[exc_class] = f @@ -1412,7 +1410,7 @@ class Flask(_PackageBoundObject): """Finds a registered error handler for the request’s blueprint. If neither blueprint nor App has a suitable handler registered, returns None """ - exc_class, code = self._ensure_exc_class(e) + exc_class, code = self._get_exc_class_and_code(type(e)) def find_superclass(d): if not d: @@ -1422,6 +1420,7 @@ class Flask(_PackageBoundObject): return None handler = d.get(superclass) if handler is not None: + d[exc_class] = handler # cache for next time exc_class is raised return handler return None From b31252db55cfac4605ad917d34eaa55fff248a38 Mon Sep 17 00:00:00 2001 From: Phil Schaf Date: Sat, 11 Apr 2015 14:21:02 +0200 Subject: [PATCH 8/8] addressed a few review concerns --- flask/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flask/app.py b/flask/app.py index 9c6d5064..6baee3a6 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1080,7 +1080,7 @@ class Flask(_PackageBoundObject): @staticmethod def _get_exc_class_and_code(exc_class_or_code): - """ensure that we register only exceptions as handler keys""" + """Ensure that we register only exceptions as handler keys""" if isinstance(exc_class_or_code, integer_types): exc_class = default_exceptions[exc_class_or_code] else: @@ -1412,15 +1412,15 @@ class Flask(_PackageBoundObject): """ exc_class, code = self._get_exc_class_and_code(type(e)) - def find_superclass(d): - if not d: + def find_superclass(handler_map): + if not handler_map: return None - for superclass in exc_class.mro(): + for superclass in exc_class.__mro__: if superclass is BaseException: return None - handler = d.get(superclass) + handler = handler_map.get(superclass) if handler is not None: - d[exc_class] = handler # cache for next time exc_class is raised + handler_map[exc_class] = handler # cache for next time exc_class is raised return handler return None