Merge pull request #4487 from pallets/typing-errorhandler

relax `errorhandler` function arg type
This commit is contained in:
David Lord 2022-03-15 08:36:16 -07:00 committed by GitHub
commit cb7cd1e79b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 34 additions and 36 deletions

View file

@ -47,6 +47,9 @@ Unreleased
loader thread. :issue:`4460` loader thread. :issue:`4460`
- Deleting the session cookie uses the ``httponly`` flag. - Deleting the session cookie uses the ``httponly`` flag.
:issue:`4485` :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 Version 2.0.3

View file

@ -1265,9 +1265,7 @@ class Flask(Scaffold):
self.shell_context_processors.append(f) self.shell_context_processors.append(f)
return f return f
def _find_error_handler( def _find_error_handler(self, e: Exception) -> t.Optional["ErrorHandlerCallable"]:
self, e: Exception
) -> t.Optional["ErrorHandlerCallable[Exception]"]:
"""Return a registered error handler for an exception in this order: """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 a specific code, app handler for a specific code,
blueprint handler for an exception class, app handler for an exception blueprint handler for an exception class, app handler for an exception
@ -1674,13 +1672,13 @@ class Flask(Scaffold):
# a 3-tuple is unpacked directly # a 3-tuple is unpacked directly
if len_rv == 3: if len_rv == 3:
rv, status, headers = rv rv, status, headers = rv # type: ignore[misc]
# decide if a 2-tuple has status or headers # decide if a 2-tuple has status or headers
elif len_rv == 2: elif len_rv == 2:
if isinstance(rv[1], (Headers, dict, tuple, list)): if isinstance(rv[1], (Headers, dict, tuple, list)):
rv, headers = rv rv, headers = rv
else: else:
rv, status = rv rv, status = rv # type: ignore[misc]
# other sized tuples are not allowed # other sized tuples are not allowed
else: else:
raise TypeError( raise TypeError(
@ -1703,7 +1701,11 @@ class Flask(Scaffold):
# let the response class set the status and headers instead of # let the response class set the status and headers instead of
# waiting to do it manually, so that the class can handle any # waiting to do it manually, so that the class can handle any
# special logic # 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 status = headers = None
elif isinstance(rv, dict): elif isinstance(rv, dict):
rv = jsonify(rv) rv = jsonify(rv)
@ -1731,13 +1733,13 @@ class Flask(Scaffold):
# prefer the status if it was provided # prefer the status if it was provided
if status is not None: if status is not None:
if isinstance(status, (str, bytes, bytearray)): if isinstance(status, (str, bytes, bytearray)):
rv.status = status # type: ignore rv.status = status
else: else:
rv.status_code = status rv.status_code = status
# extend existing headers with provided headers # extend existing headers with provided headers
if headers: if headers:
rv.headers.update(headers) rv.headers.update(headers) # type: ignore[arg-type]
return rv return rv

View file

@ -574,9 +574,7 @@ class Blueprint(Scaffold):
handler is used for all requests, even if outside of the blueprint. handler is used for all requests, even if outside of the blueprint.
""" """
def decorator( def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable":
f: "ErrorHandlerCallable[Exception]",
) -> "ErrorHandlerCallable[Exception]":
self.record_once(lambda s: s.app.errorhandler(code)(f)) self.record_once(lambda s: s.app.errorhandler(code)(f))
return f return f

View file

@ -186,7 +186,7 @@ def make_response(*args: t.Any) -> "Response":
return current_app.response_class() return current_app.response_class()
if len(args) == 1: if len(args) == 1:
args = args[0] 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: def url_for(endpoint: str, **values: t.Any) -> str:

View file

@ -21,7 +21,6 @@ from .templating import _default_template_ctx_processor
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import AppOrBlueprintKey from .typing import AppOrBlueprintKey
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import GenericException
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
from .typing import URLDefaultCallable from .typing import URLDefaultCallable
@ -145,10 +144,7 @@ class Scaffold:
#: directly and its format may change at any time. #: directly and its format may change at any time.
self.error_handler_spec: t.Dict[ self.error_handler_spec: t.Dict[
AppOrBlueprintKey, AppOrBlueprintKey,
t.Dict[ t.Dict[t.Optional[int], t.Dict[t.Type[Exception], "ErrorHandlerCallable"]],
t.Optional[int],
t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
],
] = defaultdict(lambda: defaultdict(dict)) ] = defaultdict(lambda: defaultdict(dict))
#: A data structure of functions to call at the beginning of #: A data structure of functions to call at the beginning of
@ -652,11 +648,8 @@ class Scaffold:
@setupmethod @setupmethod
def errorhandler( def errorhandler(
self, code_or_exception: t.Union[t.Type[GenericException], int] self, code_or_exception: t.Union[t.Type[Exception], int]
) -> t.Callable[ ) -> t.Callable[["ErrorHandlerCallable"], "ErrorHandlerCallable"]:
["ErrorHandlerCallable[GenericException]"],
"ErrorHandlerCallable[GenericException]",
]:
"""Register a function to handle errors by code or exception class. """Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an A decorator that is used to register a function given an
@ -686,9 +679,7 @@ class Scaffold:
an arbitrary exception an arbitrary exception
""" """
def decorator( def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable":
f: "ErrorHandlerCallable[GenericException]",
) -> "ErrorHandlerCallable[GenericException]":
self.register_error_handler(code_or_exception, f) self.register_error_handler(code_or_exception, f)
return f return f
@ -697,8 +688,8 @@ class Scaffold:
@setupmethod @setupmethod
def register_error_handler( def register_error_handler(
self, self,
code_or_exception: t.Union[t.Type[GenericException], int], code_or_exception: t.Union[t.Type[Exception], int],
f: "ErrorHandlerCallable[GenericException]", f: "ErrorHandlerCallable",
) -> None: ) -> None:
"""Alternative error attach function to the :meth:`errorhandler` """Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator decorator that is more straightforward to use for non decorator
@ -722,9 +713,7 @@ class Scaffold:
" instead." " instead."
) from None ) from None
self.error_handler_spec[None][code][exc_class] = t.cast( self.error_handler_spec[None][code][exc_class] = f
"ErrorHandlerCallable[Exception]", f
)
@staticmethod @staticmethod
def _get_exc_class_and_code( def _get_exc_class_and_code(

View file

@ -4,14 +4,16 @@ import typing as t
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from _typeshed.wsgi import WSGIApplication # noqa: F401 from _typeshed.wsgi import WSGIApplication # noqa: F401
from werkzeug.datastructures import Headers # 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. # The possible types that are directly convertible or are a Response object.
ResponseValue = t.Union[ ResponseValue = t.Union[
"Response", "Response",
t.AnyStr, str,
bytes,
t.Dict[str, t.Any], # any jsonify-able dict t.Dict[str, t.Any], # any jsonify-able dict
t.Generator[t.AnyStr, None, None], t.Iterator[str],
t.Iterator[bytes],
] ]
StatusCode = int StatusCode = int
@ -33,8 +35,6 @@ ResponseReturnValue = t.Union[
"WSGIApplication", "WSGIApplication",
] ]
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Callable[["Response"], "Response"] AfterRequestCallable = t.Callable[["Response"], "Response"]
BeforeFirstRequestCallable = t.Callable[[], None] BeforeFirstRequestCallable = t.Callable[[], None]
@ -46,4 +46,10 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool] TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None] URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[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]