diff --git a/CHANGES.rst b/CHANGES.rst index fd46c84c..7da206f1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -47,6 +47,9 @@ Unreleased loader thread. :issue:`4460` - Deleting the session cookie uses the ``httponly`` flag. :issue:`4485` +- Relax typing for ``errorhandler`` to allow the user to use more + precise types and decorate the same function multiple times. + :issue:`4095, 4295, 4297` Version 2.0.3 diff --git a/src/flask/app.py b/src/flask/app.py index aab5745d..854a796e 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1265,9 +1265,7 @@ class Flask(Scaffold): self.shell_context_processors.append(f) return f - def _find_error_handler( - self, e: Exception - ) -> t.Optional["ErrorHandlerCallable[Exception]"]: + def _find_error_handler(self, e: Exception) -> t.Optional["ErrorHandlerCallable"]: """Return a registered error handler for an exception in this order: blueprint handler for a specific code, app handler for a specific code, blueprint handler for an exception class, app handler for an exception diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 47465ad7..5d3b4e22 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -574,9 +574,7 @@ class Blueprint(Scaffold): handler is used for all requests, even if outside of the blueprint. """ - def decorator( - f: "ErrorHandlerCallable[Exception]", - ) -> "ErrorHandlerCallable[Exception]": + def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": self.record_once(lambda s: s.app.errorhandler(code)(f)) return f diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 1d134435..acf8708b 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -21,7 +21,6 @@ from .templating import _default_template_ctx_processor from .typing import AfterRequestCallable from .typing import AppOrBlueprintKey from .typing import BeforeRequestCallable -from .typing import GenericException from .typing import TeardownCallable from .typing import TemplateContextProcessorCallable from .typing import URLDefaultCallable @@ -145,10 +144,7 @@ class Scaffold: #: directly and its format may change at any time. self.error_handler_spec: t.Dict[ AppOrBlueprintKey, - t.Dict[ - t.Optional[int], - t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"], - ], + t.Dict[t.Optional[int], t.Dict[t.Type[Exception], "ErrorHandlerCallable"]], ] = defaultdict(lambda: defaultdict(dict)) #: A data structure of functions to call at the beginning of @@ -652,11 +648,8 @@ class Scaffold: @setupmethod def errorhandler( - self, code_or_exception: t.Union[t.Type[GenericException], int] - ) -> t.Callable[ - ["ErrorHandlerCallable[GenericException]"], - "ErrorHandlerCallable[GenericException]", - ]: + self, code_or_exception: t.Union[t.Type[Exception], int] + ) -> t.Callable[["ErrorHandlerCallable"], "ErrorHandlerCallable"]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an @@ -686,9 +679,7 @@ class Scaffold: an arbitrary exception """ - def decorator( - f: "ErrorHandlerCallable[GenericException]", - ) -> "ErrorHandlerCallable[GenericException]": + def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable": self.register_error_handler(code_or_exception, f) return f @@ -697,8 +688,8 @@ class Scaffold: @setupmethod def register_error_handler( self, - code_or_exception: t.Union[t.Type[GenericException], int], - f: "ErrorHandlerCallable[GenericException]", + code_or_exception: t.Union[t.Type[Exception], int], + f: "ErrorHandlerCallable", ) -> None: """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator @@ -722,9 +713,7 @@ class Scaffold: " instead." ) from None - self.error_handler_spec[None][code][exc_class] = t.cast( - "ErrorHandlerCallable[Exception]", f - ) + self.error_handler_spec[None][code][exc_class] = f @staticmethod def _get_exc_class_and_code( diff --git a/src/flask/typing.py b/src/flask/typing.py index 93896f80..bd8e61dc 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -33,8 +33,6 @@ ResponseReturnValue = t.Union[ "WSGIApplication", ] -GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True) - AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AfterRequestCallable = t.Callable[["Response"], "Response"] BeforeFirstRequestCallable = t.Callable[[], None] @@ -46,4 +44,10 @@ TemplateGlobalCallable = t.Callable[..., t.Any] TemplateTestCallable = t.Callable[..., bool] URLDefaultCallable = t.Callable[[str, dict], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] -ErrorHandlerCallable = t.Callable[[GenericException], ResponseReturnValue] +# This should take Exception, but that either breaks typing the argument +# with a specific exception, or decorating multiple times with different +# exceptions (and using a union type on the argument). +# https://github.com/pallets/flask/issues/4095 +# https://github.com/pallets/flask/issues/4295 +# https://github.com/pallets/flask/issues/4297 +ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue]