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..3a4df366 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 @@ -1674,13 +1672,13 @@ class Flask(Scaffold): # a 3-tuple is unpacked directly if len_rv == 3: - rv, status, headers = rv + rv, status, headers = rv # type: ignore[misc] # decide if a 2-tuple has status or headers elif len_rv == 2: if isinstance(rv[1], (Headers, dict, tuple, list)): rv, headers = rv else: - rv, status = rv + rv, status = rv # type: ignore[misc] # other sized tuples are not allowed else: raise TypeError( @@ -1703,7 +1701,11 @@ class Flask(Scaffold): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic - rv = self.response_class(rv, status=status, headers=headers) + rv = self.response_class( + rv, + status=status, + headers=headers, # type: ignore[arg-type] + ) status = headers = None elif isinstance(rv, dict): rv = jsonify(rv) @@ -1731,13 +1733,13 @@ class Flask(Scaffold): # prefer the status if it was provided if status is not None: if isinstance(status, (str, bytes, bytearray)): - rv.status = status # type: ignore + rv.status = status else: rv.status_code = status # extend existing headers with provided headers if headers: - rv.headers.update(headers) + rv.headers.update(headers) # type: ignore[arg-type] return rv 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/helpers.py b/src/flask/helpers.py index 84059319..fe47500e 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -186,7 +186,7 @@ def make_response(*args: t.Any) -> "Response": return current_app.response_class() if len(args) == 1: args = args[0] - return current_app.make_response(args) + return current_app.make_response(args) # type: ignore def url_for(endpoint: str, **values: t.Any) -> str: 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..a839a7e4 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -4,14 +4,16 @@ import typing as t if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication # noqa: F401 from werkzeug.datastructures import Headers # noqa: F401 - from .wrappers import Response # noqa: F401 + from werkzeug.wrappers.response import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ "Response", - t.AnyStr, + str, + bytes, t.Dict[str, t.Any], # any jsonify-able dict - t.Generator[t.AnyStr, None, None], + t.Iterator[str], + t.Iterator[bytes], ] StatusCode = int @@ -33,8 +35,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 +46,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]