From 96ecda4426bacd8770fc6f1a02112ecf1e7da386 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 16 Aug 2023 20:03:31 +0200 Subject: [PATCH 01/65] feat: Type app helpers module --- falcon/app_helpers.py | 11 ++++++----- pyproject.toml | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 38b591914..1c743ec2d 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -13,6 +13,7 @@ # limitations under the License. """Utilities for the App class.""" +from __future__ import annotations from inspect import iscoroutinefunction from typing import IO, Iterable, List, Tuple @@ -201,7 +202,7 @@ def prepare_middleware_ws(middleware: Iterable) -> Tuple[list, list]: return request_mw, resource_mw -def default_serialize_error(req: Request, resp: Response, exception: HTTPError): +def default_serialize_error(req: Request, resp: Response, exception: HTTPError) -> None: """Serialize the given instance of HTTPError. This function determines which of the supported media types, if @@ -280,14 +281,14 @@ class CloseableStreamIterator: block_size (int): Number of bytes to read per iteration. """ - def __init__(self, stream: IO, block_size: int): + def __init__(self, stream: IO, block_size: int) -> None: self._stream = stream self._block_size = block_size - def __iter__(self): + def __iter__(self) -> CloseableStreamIterator: return self - def __next__(self): + def __next__(self) -> bytes: data = self._stream.read(self._block_size) if data == b'': @@ -295,7 +296,7 @@ def __next__(self): else: return data - def close(self): + def close(self) -> None: try: self._stream.close() except (AttributeError, TypeError): diff --git a/pyproject.toml b/pyproject.toml index ad445ce55..c1261ec07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ [[tool.mypy.overrides]] module = [ "falcon.stream", - "falcon.util.*" + "falcon.util.*", + "falcon.app_helpers", ] disallow_untyped_defs = true From 00739d10482e10e4fbddf074496572c1dbe73f7d Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 16 Aug 2023 22:07:28 +0200 Subject: [PATCH 02/65] feat: Add typing to errors module --- falcon/errors.py | 307 ++++++++++++++++++++++++++++++++++++++++------- pyproject.toml | 3 + 2 files changed, 266 insertions(+), 44 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index cdad7ffa7..bfbdfcf5f 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -35,7 +35,12 @@ def on_get(self, req, resp): """ from datetime import datetime +from typing import Dict +from typing import Iterable +from typing import List from typing import Optional +from typing import Tuple +from typing import Union from falcon.http_error import HTTPError import falcon.status_codes as status @@ -143,7 +148,7 @@ class WebSocketDisconnected(ConnectionError): code (int): The WebSocket close code, as per the WebSocket spec. """ - def __init__(self, code: Optional[int] = None): + def __init__(self, code: Optional[int] = None) -> None: self.code = code or 1000 # Default to "Normal Closure" @@ -169,6 +174,12 @@ class WebSocketServerError(WebSocketDisconnected): pass +Kwargs = Union[str, int, None] +RetryAfter = Union[int, datetime, None] +NormalizedHeaders = Dict[str, str] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] + + class HTTPBadRequest(HTTPError): """400 Bad Request. @@ -215,7 +226,13 @@ class HTTPBadRequest(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ) -> None: super().__init__( status.HTTP_400, title=title, @@ -293,7 +310,12 @@ class HTTPUnauthorized(HTTPError): @deprecated_args(allowed_positional=0) def __init__( - self, title=None, description=None, headers=None, challenges=None, **kwargs + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + challenges: Optional[Iterable[str]] = None, + **kwargs: Kwargs, ): if challenges: headers = _load_headers(headers) @@ -365,7 +387,13 @@ class HTTPForbidden(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_403, title=title, @@ -430,7 +458,13 @@ class HTTPNotFound(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_404, title=title, @@ -551,7 +585,12 @@ class HTTPMethodNotAllowed(HTTPError): @deprecated_args(allowed_positional=1) def __init__( - self, allowed_methods, title=None, description=None, headers=None, **kwargs + self, + allowed_methods: List[str], + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, ): headers = _load_headers(headers) headers['Allow'] = ', '.join(allowed_methods) @@ -617,7 +656,13 @@ class HTTPNotAcceptable(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_406, title=title, @@ -684,7 +729,13 @@ class HTTPConflict(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_409, title=title, @@ -757,7 +808,13 @@ class HTTPGone(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_410, title=title, @@ -815,7 +872,13 @@ class HTTPLengthRequired(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_411, title=title, @@ -874,7 +937,13 @@ class HTTPPreconditionFailed(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_412, title=title, @@ -944,8 +1013,13 @@ class HTTPPayloadTooLarge(HTTPError): @deprecated_args(allowed_positional=0) def __init__( - self, title=None, description=None, retry_after=None, headers=None, **kwargs - ): + self, + title: Optional[str] = None, + description: Optional[str] = None, + retry_after: RetryAfter = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ) -> None: super().__init__( status.HTTP_413, title=title, @@ -1009,7 +1083,13 @@ class HTTPUriTooLong(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_414, title=title, @@ -1068,7 +1148,13 @@ class HTTPUnsupportedMediaType(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_415, title=title, @@ -1141,7 +1227,12 @@ class HTTPRangeNotSatisfiable(HTTPError): @deprecated_args(allowed_positional=1) def __init__( - self, resource_length, title=None, description=None, headers=None, **kwargs + self, + resource_length: int, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, ): headers = _load_headers(headers) headers['Content-Range'] = 'bytes */' + str(resource_length) @@ -1206,7 +1297,13 @@ class HTTPUnprocessableEntity(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_422, title=title, @@ -1262,7 +1359,13 @@ class HTTPLocked(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_423, title=title, @@ -1317,7 +1420,13 @@ class HTTPFailedDependency(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_424, title=title, @@ -1380,7 +1489,13 @@ class HTTPPreconditionRequired(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_428, title=title, @@ -1449,7 +1564,12 @@ class HTTPTooManyRequests(HTTPError): @deprecated_args(allowed_positional=0) def __init__( - self, title=None, description=None, headers=None, retry_after=None, **kwargs + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + retry_after: RetryAfter = None, + **kwargs: Kwargs, ): super().__init__( status.HTTP_429, @@ -1512,7 +1632,13 @@ class HTTPRequestHeaderFieldsTooLarge(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_431, title=title, @@ -1581,7 +1707,13 @@ class HTTPUnavailableForLegalReasons(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_451, title=title, @@ -1636,7 +1768,13 @@ class HTTPInternalServerError(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_500, title=title, @@ -1698,7 +1836,13 @@ class HTTPNotImplemented(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_501, title=title, @@ -1753,7 +1897,13 @@ class HTTPBadGateway(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_502, title=title, @@ -1825,7 +1975,12 @@ class HTTPServiceUnavailable(HTTPError): @deprecated_args(allowed_positional=0) def __init__( - self, title=None, description=None, headers=None, retry_after=None, **kwargs + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + retry_after: RetryAfter = None, + **kwargs: Kwargs, ): super().__init__( status.HTTP_503, @@ -1882,7 +2037,13 @@ class HTTPGatewayTimeout(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_504, title=title, @@ -1943,7 +2104,13 @@ class HTTPVersionNotSupported(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_505, title=title, @@ -2002,7 +2169,13 @@ class HTTPInsufficientStorage(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_507, title=title, @@ -2058,7 +2231,13 @@ class HTTPLoopDetected(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_508, title=title, @@ -2126,7 +2305,13 @@ class HTTPNetworkAuthenticationRequired(HTTPError): """ @deprecated_args(allowed_positional=0) - def __init__(self, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): super().__init__( status.HTTP_511, title=title, @@ -2179,7 +2364,13 @@ class HTTPInvalidHeader(HTTPBadRequest): """ @deprecated_args(allowed_positional=2) - def __init__(self, msg, header_name, headers=None, **kwargs): + def __init__( + self, + msg: str, + header_name: str, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): description = 'The value provided for the "{0}" header is invalid. {1}' description = description.format(header_name, msg) @@ -2233,7 +2424,12 @@ class HTTPMissingHeader(HTTPBadRequest): """ @deprecated_args(allowed_positional=1) - def __init__(self, header_name, headers=None, **kwargs): + def __init__( + self, + header_name: str, + headers: RawHeaders = None, + **kwargs: Kwargs, + ): description = 'The "{0}" header is required.' description = description.format(header_name) @@ -2290,7 +2486,13 @@ class HTTPInvalidParam(HTTPBadRequest): """ @deprecated_args(allowed_positional=2) - def __init__(self, msg, param_name, headers=None, **kwargs): + def __init__( + self, + msg: str, + param_name: str, + headers: RawHeaders = None, + **kwargs: Kwargs, + ) -> None: description = 'The "{0}" parameter is invalid. {1}' description = description.format(param_name, msg) @@ -2346,7 +2548,12 @@ class HTTPMissingParam(HTTPBadRequest): """ @deprecated_args(allowed_positional=1) - def __init__(self, param_name, headers=None, **kwargs): + def __init__( + self, + param_name: str, + headers: RawHeaders = None, + **kwargs: Kwargs, + ) -> None: description = 'The "{0}" parameter is required.' description = description.format(param_name) @@ -2397,7 +2604,7 @@ class MediaNotFoundError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type, **kwargs): + def __init__(self, media_type: str, **kwargs: Kwargs) -> None: super().__init__( title='Invalid {0}'.format(media_type), description='Could not parse an empty {0} body'.format(media_type), @@ -2442,21 +2649,21 @@ class MediaMalformedError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type, **kwargs): + def __init__(self, media_type: str, **kwargs: Union[RawHeaders, Kwargs]): super().__init__( title='Invalid {0}'.format(media_type), description=None, **kwargs ) self._media_type = media_type @property - def description(self): + def description(self) -> Optional[str]: msg = 'Could not parse {} body'.format(self._media_type) if self.__cause__ is not None: msg += ' - {}'.format(self.__cause__) return msg @description.setter - def description(self, value): + def description(self, value: str) -> None: pass @@ -2505,7 +2712,14 @@ class MediaValidationError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, *, title=None, description=None, headers=None, **kwargs): + def __init__( + self, + *, + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + **kwargs: Kwargs, + ) -> None: super().__init__( title=title, description=description, @@ -2535,7 +2749,9 @@ class MultipartParseError(MediaMalformedError): description = None @deprecated_args(allowed_positional=0) - def __init__(self, description=None, **kwargs): + def __init__( + self, description: Optional[str] = None, **kwargs: Union[RawHeaders, Kwargs] + ) -> None: HTTPBadRequest.__init__( self, title='Malformed multipart/form-data request media', @@ -2549,7 +2765,7 @@ def __init__(self, description=None, **kwargs): # ----------------------------------------------------------------------------- -def _load_headers(headers): +def _load_headers(headers: RawHeaders) -> NormalizedHeaders: """Transform the headers to dict.""" if headers is None: return {} @@ -2558,7 +2774,10 @@ def _load_headers(headers): return dict(headers) -def _parse_retry_after(headers, retry_after): +def _parse_retry_after( + headers: RawHeaders, + retry_after: RetryAfter, +) -> RawHeaders: """Set the Retry-After to the headers when required.""" if retry_after is None: return headers diff --git a/pyproject.toml b/pyproject.toml index c1261ec07..669568818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,9 @@ "falcon.stream", "falcon.util.*", "falcon.app_helpers", + "falcon.asgi_spec", + "falcon.constants", + "falcon.errors", ] disallow_untyped_defs = true From b550754817822325914628a0a1a9e5f9b595e0d9 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 16 Aug 2023 22:17:33 +0200 Subject: [PATCH 03/65] feat: Add typings to forwarded module --- falcon/forwarded.py | 14 ++++++++------ pyproject.toml | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/falcon/forwarded.py b/falcon/forwarded.py index ba91975fb..64bb32dc7 100644 --- a/falcon/forwarded.py +++ b/falcon/forwarded.py @@ -19,6 +19,8 @@ import re import string +from typing import List +from typing import Optional from falcon.util.uri import unquote_string @@ -76,14 +78,14 @@ class Forwarded: # falcon.Request interface. __slots__ = ('src', 'dest', 'host', 'scheme') - def __init__(self): - self.src = None - self.dest = None - self.host = None - self.scheme = None + def __init__(self) -> None: + self.src: Optional[str] = None + self.dest: Optional[str] = None + self.host: Optional[str] = None + self.scheme: Optional[str] = None -def _parse_forwarded_header(forwarded): +def _parse_forwarded_header(forwarded: str) -> List[Forwarded]: """Parse the value of a Forwarded header. Makes an effort to parse Forwarded headers as specified by RFC 7239: diff --git a/pyproject.toml b/pyproject.toml index 669568818..ab1e5d472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ "falcon.asgi_spec", "falcon.constants", "falcon.errors", + "falcon.forwarded", ] disallow_untyped_defs = true From ccc5c7c42b908acd1477d7e81c154a181c20a664 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 11:20:37 +0200 Subject: [PATCH 04/65] feat: Add typing to hooks --- falcon/hooks.py | 95 +++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 3 +- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 4172e9da6..af79bedbe 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -18,7 +18,18 @@ from inspect import getmembers from inspect import iscoroutinefunction import re - +from typing import Any +from typing import Awaitable +from typing import Callable +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union + +from falcon import Request as SynchronousRequest +from falcon import Response as SynchronousResponse +from falcon.asgi import Request as AsynchronousRequest +from falcon.asgi import Response as AsynchronousResponse from falcon.constants import COMBINED_METHODS from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe @@ -28,8 +39,14 @@ r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS)) ) +SynchronousResource = Callable[..., Any] +AsynchronousResource = Callable[..., Awaitable[Any]] +Resource = Union[SynchronousResource, AsynchronousResource] + -def before(action, *args, is_async=False, **kwargs): +def before( + action: Resource, *args: Any, is_async: bool = False, **kwargs: Any +) -> Callable[[Resource], Resource]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -79,7 +96,7 @@ def do_something(req, resp, resource, params): *action*. """ - def _before(responder_or_resource): + def _before(responder_or_resource: Resource) -> Resource: if isinstance(responder_or_resource, type): resource = responder_or_resource @@ -89,7 +106,7 @@ def _before(responder_or_resource): # responder in the do_before_all closure; otherwise, they # will capture the same responder variable that is shared # between iterations of the for loop, above. - def let(responder=responder): + def let(responder: Resource = responder) -> None: do_before_all = _wrap_with_before( responder, action, args, kwargs, is_async ) @@ -109,7 +126,9 @@ def let(responder=responder): return _before -def after(action, *args, is_async=False, **kwargs): +def after( + action: Resource, *args: Any, is_async: bool = False, **kwargs: Any +) -> Callable[[Resource], Resource]: """Execute the given action function *after* the responder. Args: @@ -142,14 +161,14 @@ def after(action, *args, is_async=False, **kwargs): *action*. """ - def _after(responder_or_resource): + def _after(responder_or_resource: Resource) -> Resource: if isinstance(responder_or_resource, type): resource = responder_or_resource for responder_name, responder in getmembers(resource, callable): if _DECORABLE_METHOD_NAME.match(responder_name): - def let(responder=responder): + def let(responder: Resource = responder) -> None: do_after_all = _wrap_with_after( responder, action, args, kwargs, is_async ) @@ -174,7 +193,13 @@ def let(responder=responder): # ----------------------------------------------------------------------------- -def _wrap_with_after(responder, action, action_args, action_kwargs, is_async): +def _wrap_with_after( + responder: Resource, + action: Resource, + action_args: Any, + action_kwargs: Any, + is_async: bool, +) -> Resource: """Execute the given action function after a responder method. Args: @@ -197,20 +222,33 @@ def _wrap_with_after(responder, action, action_args, action_kwargs, is_async): # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - action = _wrap_non_coroutine_unsafe(action) + wrapped_action = _wrap_non_coroutine_unsafe(action) @wraps(responder) - async def do_after(self, req, resp, *args, **kwargs): + async def do_after( + self: Resource, + req: AsynchronousRequest, + resp: AsynchronousResponse, + *args: Any, + **kwargs: Any, + ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) await responder(self, req, resp, **kwargs) - await action(req, resp, self, *action_args, **action_kwargs) + assert wrapped_action + await wrapped_action(req, resp, self, *action_args, **action_kwargs) else: @wraps(responder) - def do_after(self, req, resp, *args, **kwargs): + def do_after( + self: Resource, + req: SynchronousRequest, + resp: SynchronousResponse, + *args: Any, + **kwargs: Any, + ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -220,7 +258,13 @@ def do_after(self, req, resp, *args, **kwargs): return do_after -def _wrap_with_before(responder, action, action_args, action_kwargs, is_async): +def _wrap_with_before( + responder: Resource, + action: Resource, + action_args: Tuple[Any, ...], + action_kwargs: Dict[str, Any], + is_async: bool, +) -> Union[Callable[..., Awaitable[None]], Callable[..., None]]: """Execute the given action function before a responder method. Args: @@ -243,20 +287,33 @@ def _wrap_with_before(responder, action, action_args, action_kwargs, is_async): # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - action = _wrap_non_coroutine_unsafe(action) + wrapped_action = _wrap_non_coroutine_unsafe(action) @wraps(responder) - async def do_before(self, req, resp, *args, **kwargs): + async def do_before( + self: Resource, + req: AsynchronousRequest, + resp: AsynchronousResponse, + *args: Any, + **kwargs: Any, + ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) - await action(req, resp, self, kwargs, *action_args, **action_kwargs) + assert wrapped_action + await wrapped_action(req, resp, self, kwargs, *action_args, **action_kwargs) await responder(self, req, resp, **kwargs) else: @wraps(responder) - def do_before(self, req, resp, *args, **kwargs): + def do_before( + self: Resource, + req: SynchronousRequest, + resp: SynchronousResponse, + *args: Any, + **kwargs: Any, + ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -266,7 +323,9 @@ def do_before(self, req, resp, *args, **kwargs): return do_before -def _merge_responder_args(args, kwargs, argnames): +def _merge_responder_args( + args: Tuple[Any, ...], kwargs: Dict[str, Any], argnames: List[Any] +) -> None: """Merge responder args into kwargs. The framework always passes extra args as keyword arguments. diff --git a/pyproject.toml b/pyproject.toml index ab1e5d472..8bb51dc3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,14 @@ [[tool.mypy.overrides]] module = [ - "falcon.stream", "falcon.util.*", "falcon.app_helpers", "falcon.asgi_spec", "falcon.constants", "falcon.errors", "falcon.forwarded", + "falcon.hooks", + "falcon.stream", ] disallow_untyped_defs = true From 0fd6d7ad58c0c8ecd10a1246c2bdb15796a75c38 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 18:43:42 +0200 Subject: [PATCH 05/65] feat: Add typing to falcon hooks --- falcon/hooks.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index af79bedbe..93c84ed2a 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -26,11 +26,11 @@ from typing import Tuple from typing import Union -from falcon import Request as SynchronousRequest -from falcon import Response as SynchronousResponse -from falcon.asgi import Request as AsynchronousRequest -from falcon.asgi import Response as AsynchronousResponse +from falcon.asgi.request import Request as AsynchronousRequest +from falcon.asgi.response import Response as AsynchronousResponse from falcon.constants import COMBINED_METHODS +from falcon.request import Request as SynchronousRequest +from falcon.response import Response as SynchronousResponse from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe @@ -222,7 +222,9 @@ def _wrap_with_after( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - wrapped_action = _wrap_non_coroutine_unsafe(action) + async_action = _wrap_non_coroutine_unsafe(action) + else: + async_action = action @wraps(responder) async def do_after( @@ -236,8 +238,8 @@ async def do_after( _merge_responder_args(args, kwargs, extra_argnames) await responder(self, req, resp, **kwargs) - assert wrapped_action - await wrapped_action(req, resp, self, *action_args, **action_kwargs) + assert async_action + await async_action(req, resp, self, *action_args, **action_kwargs) else: @@ -287,7 +289,9 @@ def _wrap_with_before( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - wrapped_action = _wrap_non_coroutine_unsafe(action) + async_action = _wrap_non_coroutine_unsafe(action) + else: + async_action = action @wraps(responder) async def do_before( @@ -300,8 +304,8 @@ async def do_before( if args: _merge_responder_args(args, kwargs, extra_argnames) - assert wrapped_action - await wrapped_action(req, resp, self, kwargs, *action_args, **action_kwargs) + assert async_action + await async_action(req, resp, self, kwargs, *action_args, **action_kwargs) await responder(self, req, resp, **kwargs) else: From 4a7daf009429bf73f25037b07a08fdf8db1b7582 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 20:48:53 +0200 Subject: [PATCH 06/65] feat: Add typing to http_error module --- falcon/http_error.py | 50 +++++++++++++++++++++++++++++++++----------- pyproject.toml | 1 + 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/falcon/http_error.py b/falcon/http_error.py index ad63c7ab7..ae50aedf3 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -13,14 +13,36 @@ # limitations under the License. """HTTPError exception class.""" - from collections import OrderedDict +import http +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union import xml.etree.ElementTree as et +try: + from typing import Protocol # type: ignore +except ImportError: # pragma: no cover + from typing_extensions import Protocol # type: ignore + from falcon.constants import MEDIA_JSON from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args +NormalizedHeaders = Dict[str, str] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] +Link = Dict[str, str] + + +class Serializer(Protocol): # pragma: no cover + def serialize( + self, media: Dict[str, Union[str, int, None, Link]], content_type: str + ) -> bytes: + ... + class HTTPError(Exception): """Represents a generic HTTP error. @@ -110,13 +132,13 @@ class HTTPError(Exception): @deprecated_args(allowed_positional=1) def __init__( self, - status, - title=None, - description=None, - headers=None, - href=None, - href_text=None, - code=None, + status: Union[http.HTTPStatus, str, int], + title: Optional[str] = None, + description: Optional[str] = None, + headers: RawHeaders = None, + href: Optional[str] = None, + href_text: Optional[str] = None, + code: Optional[int] = None, ): self.status = status @@ -129,6 +151,7 @@ def __init__( self.description = description self.headers = headers self.code = code + self.link: Optional[Link] if href: link = self.link = OrderedDict() @@ -138,7 +161,7 @@ def __init__( else: self.link = None - def __repr__(self): + def __repr__(self) -> str: return '<%s: %s>' % (self.__class__.__name__, self.status) __str__ = __repr__ @@ -147,7 +170,9 @@ def __repr__(self): def status_code(self) -> int: return http_status_to_code(self.status) - def to_dict(self, obj_type=dict): + def to_dict( + self, obj_type: Type[Dict[str, Union[str, int, None, Link]]] = dict + ) -> Dict[str, Union[str, int, None, Link]]: """Return a basic dictionary representing the error. This method can be useful when serializing the error to hash-like @@ -178,7 +203,7 @@ def to_dict(self, obj_type=dict): return obj - def to_json(self, handler=None): + def to_json(self, handler: Optional[Serializer] = None) -> bytes: """Return a JSON representation of the error. Args: @@ -194,9 +219,10 @@ def to_json(self, handler=None): obj = self.to_dict(OrderedDict) if handler is None: handler = _DEFAULT_JSON_HANDLER + assert handler return handler.serialize(obj, MEDIA_JSON) - def to_xml(self): + def to_xml(self) -> bytes: """Return an XML-encoded representation of the error. Returns: diff --git a/pyproject.toml b/pyproject.toml index 8bb51dc3d..f45053700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ "falcon.errors", "falcon.forwarded", "falcon.hooks", + "falcon.http_error", "falcon.stream", ] disallow_untyped_defs = true From 31e4e0d58905c34325e5666069a81455cb0c08d8 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 21:05:58 +0200 Subject: [PATCH 07/65] feat: Extract RawHeaders and NormalizedHeaders to typing module --- falcon/errors.py | 4 ++-- falcon/http_error.py | 6 ++---- falcon/typing.py | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index bfbdfcf5f..7888ba312 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -44,6 +44,8 @@ def on_get(self, req, resp): from falcon.http_error import HTTPError import falcon.status_codes as status +from falcon.typing import NormalizedHeaders +from falcon.typing import RawHeaders from falcon.util.deprecation import deprecated_args from falcon.util.misc import dt_to_http @@ -176,8 +178,6 @@ class WebSocketServerError(WebSocketDisconnected): Kwargs = Union[str, int, None] RetryAfter = Union[int, datetime, None] -NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] class HTTPBadRequest(HTTPError): diff --git a/falcon/http_error.py b/falcon/http_error.py index ae50aedf3..c8eceb59a 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -16,13 +16,13 @@ from collections import OrderedDict import http from typing import Dict -from typing import List from typing import Optional -from typing import Tuple from typing import Type from typing import Union import xml.etree.ElementTree as et +from falcon.typing import RawHeaders + try: from typing import Protocol # type: ignore except ImportError: # pragma: no cover @@ -32,8 +32,6 @@ from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args -NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] Link = Dict[str, str] diff --git a/falcon/typing.py b/falcon/typing.py index 4acf8ad97..1cdf4b015 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -15,6 +15,10 @@ """Shorthand definitions for more complex types.""" from typing import Any, Callable, Pattern, Union +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union from falcon.request import Request from falcon.response import Response @@ -34,3 +38,5 @@ # arguments afterwords? # class SinkCallable(Protocol): # def __call__(sef, req: Request, resp: Response, ): ... +NormalizedHeaders = Dict[str, str] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] From 763fc7a6e2a44062fc05617fef1bc8a4e98f2124 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 21:08:14 +0200 Subject: [PATCH 08/65] feat: Extract status to typing module --- falcon/errors.py | 6 ++---- falcon/http_error.py | 7 +++---- falcon/typing.py | 8 +------- falcon/typing_http_data.py | 9 +++++++++ 4 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 falcon/typing_http_data.py diff --git a/falcon/errors.py b/falcon/errors.py index 7888ba312..c53c4ea3d 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -35,17 +35,15 @@ def on_get(self, req, resp): """ from datetime import datetime -from typing import Dict from typing import Iterable from typing import List from typing import Optional -from typing import Tuple from typing import Union from falcon.http_error import HTTPError import falcon.status_codes as status -from falcon.typing import NormalizedHeaders -from falcon.typing import RawHeaders +from falcon.typing_http_data import NormalizedHeaders +from falcon.typing_http_data import RawHeaders from falcon.util.deprecation import deprecated_args from falcon.util.misc import dt_to_http diff --git a/falcon/http_error.py b/falcon/http_error.py index c8eceb59a..b4a73c2e5 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -14,21 +14,20 @@ """HTTPError exception class.""" from collections import OrderedDict -import http from typing import Dict from typing import Optional from typing import Type from typing import Union import xml.etree.ElementTree as et -from falcon.typing import RawHeaders - try: from typing import Protocol # type: ignore except ImportError: # pragma: no cover from typing_extensions import Protocol # type: ignore from falcon.constants import MEDIA_JSON +from falcon.typing_http_data import RawHeaders +from falcon.typing_http_data import Status from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args @@ -130,7 +129,7 @@ class HTTPError(Exception): @deprecated_args(allowed_positional=1) def __init__( self, - status: Union[http.HTTPStatus, str, int], + status: Status, title: Optional[str] = None, description: Optional[str] = None, headers: RawHeaders = None, diff --git a/falcon/typing.py b/falcon/typing.py index 1cdf4b015..2ba96d7be 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -13,11 +13,7 @@ # limitations under the License. """Shorthand definitions for more complex types.""" - -from typing import Any, Callable, Pattern, Union -from typing import Dict -from typing import List -from typing import Tuple +from typing import Any, Callable, Pattern from typing import Union from falcon.request import Request @@ -38,5 +34,3 @@ # arguments afterwords? # class SinkCallable(Protocol): # def __call__(sef, req: Request, resp: Response, ): ... -NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] diff --git a/falcon/typing_http_data.py b/falcon/typing_http_data.py new file mode 100644 index 000000000..8b207d56b --- /dev/null +++ b/falcon/typing_http_data.py @@ -0,0 +1,9 @@ +import http +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union + +NormalizedHeaders = Dict[str, str] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] +Status = Union[http.HTTPStatus, str, int] From 1d13cd0408b08e8ab484ba8cb8a808050be43ca3 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 21:15:10 +0200 Subject: [PATCH 09/65] feat: Add typing to http_status module --- falcon/http_status.py | 7 ++++++- pyproject.toml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/falcon/http_status.py b/falcon/http_status.py index d4411391e..2ba89dc13 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -13,7 +13,10 @@ # limitations under the License. """HTTPStatus exception class.""" +from typing import Optional +from falcon.typing_http_data import RawHeaders +from falcon.typing_http_data import Status from falcon.util import http_status_to_code from falcon.util.deprecation import AttributeRemovedError @@ -46,7 +49,9 @@ class HTTPStatus(Exception): __slots__ = ('status', 'headers', 'text') - def __init__(self, status, headers=None, text=None): + def __init__( + self, status: Status, headers: RawHeaders = None, text: Optional[str] = None + ) -> None: self.status = status self.headers = headers self.text = text diff --git a/pyproject.toml b/pyproject.toml index f45053700..44f076e3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ "falcon.forwarded", "falcon.hooks", "falcon.http_error", + "falcon.http_status", "falcon.stream", ] disallow_untyped_defs = true From 32fbd7d66b12ab052858c7cb5cd3aef094d99105 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 22:51:47 +0200 Subject: [PATCH 10/65] feat: Add typing to inspect module --- falcon/inspect.py | 108 +++++++++++++++++++++++++++++++++++----------- pyproject.toml | 2 + 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index 62fc74c28..de8f8ef42 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -13,20 +13,33 @@ # limitations under the License. """Inspect utilities for falcon applications.""" +from __future__ import annotations + from functools import partial import inspect +from types import CodeType +from types import FrameType +from types import FunctionType +from types import MethodType +from types import ModuleType +from types import TracebackType +from typing import Any from typing import Callable # NOQA: F401 from typing import Dict # NOQA: F401 +from typing import Iterable from typing import List from typing import Optional +from typing import Tuple from typing import Type # NOQA: F401 +from typing import Union from falcon import app_helpers from falcon.app import App from falcon.routing import CompiledRouter +from falcon.routing.compiled import CompiledRouterNode -def inspect_app(app: App) -> 'AppInfo': +def inspect_app(app: App) -> AppInfo: """Inspects an application. Args: @@ -46,7 +59,7 @@ def inspect_app(app: App) -> 'AppInfo': return AppInfo(routes, middleware, static, sinks, error_handlers, app._ASGI) -def inspect_routes(app: App) -> 'List[RouteInfo]': +def inspect_routes(app: App) -> List[RouteInfo]: """Inspects the routes of an application. Args: @@ -68,7 +81,9 @@ def inspect_routes(app: App) -> 'List[RouteInfo]': return inspect_function(router) -def register_router(router_class): +def register_router( + router_class: Type, +) -> Callable[..., Callable[..., List[RouteInfo]]]: """Register a function to inspect a particular router. This decorator registers a new function for a custom router @@ -86,7 +101,7 @@ def inspect_my_router(router): already registered an error will be raised. """ - def wraps(fn): + def wraps(fn: Callable[..., List[RouteInfo]]) -> Callable[..., List[RouteInfo]]: if router_class in _supported_routers: raise ValueError( 'Another function is already registered' @@ -98,8 +113,7 @@ def wraps(fn): return wraps -# router inspection registry -_supported_routers = {} # type: Dict[Type, Callable] +_supported_routers: Dict[Type, Callable[..., Any]] = {} def inspect_static_routes(app: App) -> 'List[StaticRouteInfo]': @@ -133,6 +147,7 @@ def inspect_sinks(app: App) -> 'List[SinkInfo]': sinks = [] for prefix, sink, _ in app._sinks: source_info, name = _get_source_info_and_name(sink) + assert source_info info = SinkInfo(prefix.pattern, name, source_info) sinks.append(info) return sinks @@ -152,6 +167,7 @@ def inspect_error_handlers(app: App) -> 'List[ErrorHandlerInfo]': errors = [] for exc, fn in app._error_handlers.items(): source_info, name = _get_source_info_and_name(fn) + assert source_info info = ErrorHandlerInfo(exc.__name__, name, source_info, _is_internal(fn)) errors.append(info) return errors @@ -190,7 +206,9 @@ def inspect_middleware(app: App) -> 'MiddlewareInfo': if method: real_func = method[0] source_info = _get_source_info(real_func) + assert source_info methods.append(MiddlewareMethodInfo(real_func.__name__, source_info)) + assert class_source_info m_info = MiddlewareClassInfo(cls_name, class_source_info, methods) middlewareClasses.append(m_info) @@ -212,7 +230,7 @@ def inspect_compiled_router(router: CompiledRouter) -> 'List[RouteInfo]': List[RouteInfo]: A list of :class:`~.RouteInfo`. """ - def _traverse(roots, parent): + def _traverse(roots: List[CompiledRouterNode], parent: str) -> None: for root in roots: path = parent + '/' + root.raw_segment if root.resource is not None: @@ -226,13 +244,13 @@ def _traverse(roots, parent): source_info = _get_source_info(real_func) internal = _is_internal(real_func) - + assert source_info method_info = RouteMethodInfo( method, source_info, real_func.__name__, internal ) methods.append(method_info) source_info, class_name = _get_source_info_and_name(root.resource) - + assert source_info route_info = RouteInfo(path, class_name, source_info, methods) routes.append(route_info) @@ -252,7 +270,7 @@ def _traverse(roots, parent): class _Traversable: __visit_name__ = 'N/A' - def to_string(self, verbose=False, internal=False) -> str: + def to_string(self, verbose: bool = False, internal: bool = False) -> str: """Return a string representation of this class. Args: @@ -266,7 +284,7 @@ def to_string(self, verbose=False, internal=False) -> str: """ return StringVisitor(verbose, internal).process(self) - def __repr__(self): + def __repr__(self) -> str: return self.to_string() @@ -522,7 +540,9 @@ def __init__( self.error_handlers = error_handlers self.asgi = asgi - def to_string(self, verbose=False, internal=False, name='') -> str: + def to_string( + self, verbose: bool = False, internal: bool = False, name: str = '' + ) -> str: """Return a string representation of this class. Args: @@ -548,7 +568,7 @@ class InspectVisitor: Subclasses must implement ``visit_`` methods for each supported class. """ - def process(self, instance: _Traversable): + def process(self, instance: _Traversable) -> str: """Process the instance, by calling the appropriate visit method. Uses the `__visit_name__` attribute of the `instance` to obtain the method @@ -579,14 +599,16 @@ class StringVisitor(InspectVisitor): beginning of the text. Defaults to ``'Falcon App'``. """ - def __init__(self, verbose=False, internal=False, name=''): + def __init__( + self, verbose: bool = False, internal: bool = False, name: str = '' + ) -> None: self.verbose = verbose self.internal = internal self.name = name self.indent = 0 @property - def tab(self): + def tab(self) -> str: """Get the current tabulation.""" return ' ' * self.indent @@ -597,13 +619,15 @@ def visit_route_method(self, route_method: RouteMethodInfo) -> str: text += ' ({0.source_info})'.format(route_method) return text - def _methods_to_string(self, methods: List): + def _methods_to_string( + self, methods: Union[List[RouteMethodInfo], List[MiddlewareMethodInfo]] + ) -> str: """Return a string from the list of methods.""" tab = self.tab + ' ' * 3 - methods = _filter_internal(methods, self.internal) - if not methods: + filtered_methods = _filter_internal(methods, self.internal) + if not filtered_methods: return '' - text_list = [self.process(m) for m in methods] + text_list = [self.process(m) for m in filtered_methods] method_text = ['{}├── {}'.format(tab, m) for m in text_list[:-1]] method_text += ['{}└── {}'.format(tab, m) for m in text_list[-1:]] return '\n'.join(method_text) @@ -753,7 +777,17 @@ def visit_app(self, app: AppInfo) -> str: # ------------------------------------------------------------------------ -def _get_source_info(obj, default='[unknown file]'): +def _get_source_info( + obj: ModuleType + | Type[Any] + | MethodType + | FunctionType + | TracebackType + | FrameType + | CodeType + | Callable[..., Any], + default: Optional[str] = '[unknown file]', +) -> Optional[str]: """Try to get the definition file and line of obj. Return default on error. @@ -767,11 +801,20 @@ def _get_source_info(obj, default='[unknown file]'): # responders coming from cythonized modules will # appear as built-in functions, and raise a # TypeError when trying to locate the source file. - source_info = default + return default return source_info -def _get_source_info_and_name(obj): +def _get_source_info_and_name( + obj: ModuleType + | Type[Any] + | MethodType + | FunctionType + | TracebackType + | FrameType + | CodeType + | Callable[..., Any] +) -> Tuple[Optional[str], str]: """Attempt to get the definition file and line of obj and its name.""" source_info = _get_source_info(obj, None) if source_info is None: @@ -780,10 +823,20 @@ def _get_source_info_and_name(obj): name = getattr(obj, '__name__', None) if name is None: name = getattr(type(obj), '__name__', '[unknown]') + assert name return source_info, name -def _is_internal(obj): +def _is_internal( + obj: ModuleType + | Type[Any] + | MethodType + | FunctionType + | TracebackType + | FrameType + | CodeType + | Callable[..., Any] +) -> bool: """Check if the module of the object is a falcon module.""" module = inspect.getmodule(obj) if module: @@ -791,7 +844,14 @@ def _is_internal(obj): return False -def _filter_internal(iterable, return_internal): +def _filter_internal( + iterable: Union[ + Iterable[RouteMethodInfo], + Iterable[ErrorHandlerInfo], + Iterable[MiddlewareMethodInfo], + ], + return_internal: bool, +) -> Union[Iterable[_Traversable], List[_Traversable]]: """Filter the internal elements of an iterable.""" if return_internal: return iterable diff --git a/pyproject.toml b/pyproject.toml index 44f076e3b..2496cd606 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,8 @@ "falcon.hooks", "falcon.http_error", "falcon.http_status", + "falcon.http_status", + "falcon.inspect", "falcon.stream", ] disallow_untyped_defs = true From 42abb3a52d603795e019516f9971b1e1a94e7a80 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 18 Aug 2023 23:10:54 +0200 Subject: [PATCH 11/65] feat: Add typing to middleware module --- falcon/middleware.py | 24 ++++++++++++++++++++---- pyproject.toml | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/falcon/middleware.py b/falcon/middleware.py index e6e8c00be..cecaae1bd 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -1,9 +1,21 @@ +from typing import Any +from typing import Awaitable +from typing import Callable from typing import Iterable from typing import Optional from typing import Union -from .request import Request -from .response import Response +from .asgi.request import Request as AsynchronousRequest +from .asgi.response import Response as AsynchronousResponse +from .request import Request as SynchronousRequest +from .response import Response as SynchronousResponse + +Request = Union[AsynchronousRequest, SynchronousRequest] +Response = Union[AsynchronousResponse, SynchronousResponse] + +SynchronousResource = Callable[..., Any] +AsynchronousResource = Callable[..., Awaitable[Any]] +Resource = Union[SynchronousResource, AsynchronousResource] class CORSMiddleware(object): @@ -75,7 +87,9 @@ def __init__( ) self.allow_credentials = allow_credentials - def process_response(self, req: Request, resp: Response, resource, req_succeeded): + def process_response( + self, req: Request, resp: Response, resource: Resource, req_succeeded: bool + ) -> None: """Implement the CORS policy for all routes. This middleware provides a simple out-of-the box CORS policy, @@ -123,5 +137,7 @@ def process_response(self, req: Request, resp: Response, resource, req_succeeded resp.set_header('Access-Control-Allow-Headers', allow_headers) resp.set_header('Access-Control-Max-Age', '86400') # 24 hours - async def process_response_async(self, *args): + async def process_response_async( + self, request: Request, response: Response, request_succeeded: bool, *args: Any + ) -> None: self.process_response(*args) diff --git a/pyproject.toml b/pyproject.toml index 2496cd606..28dadf85e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ "falcon.http_status", "falcon.http_status", "falcon.inspect", + "falcon.middleware", "falcon.stream", ] disallow_untyped_defs = true From 2e05a288f3fd9f4dc9bc9ff11f5cf9e34e7912ad Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sat, 19 Aug 2023 19:59:41 +0200 Subject: [PATCH 12/65] feat: Replace protocol with interface --- .coveragerc | 1 + falcon/http_error.py | 16 ++-------------- falcon/link.py | 3 +++ falcon/media/base.py | 3 ++- falcon/media_serializer.py | 11 +++++++++++ falcon/middleware.py | 13 +++---------- 6 files changed, 22 insertions(+), 25 deletions(-) create mode 100644 falcon/link.py create mode 100644 falcon/media_serializer.py diff --git a/.coveragerc b/.coveragerc index 2c5041d7e..79133debb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,3 +13,4 @@ exclude_lines = pragma: nocover pragma: no cover pragma: no py39,py310 cover + raise NotImplementedError() diff --git a/falcon/http_error.py b/falcon/http_error.py index b4a73c2e5..46a123ab5 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -20,26 +20,14 @@ from typing import Union import xml.etree.ElementTree as et -try: - from typing import Protocol # type: ignore -except ImportError: # pragma: no cover - from typing_extensions import Protocol # type: ignore - from falcon.constants import MEDIA_JSON +from falcon.link import Link +from falcon.media_serializer import Serializer from falcon.typing_http_data import RawHeaders from falcon.typing_http_data import Status from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args -Link = Dict[str, str] - - -class Serializer(Protocol): # pragma: no cover - def serialize( - self, media: Dict[str, Union[str, int, None, Link]], content_type: str - ) -> bytes: - ... - class HTTPError(Exception): """Represents a generic HTTP error. diff --git a/falcon/link.py b/falcon/link.py new file mode 100644 index 000000000..51a65db40 --- /dev/null +++ b/falcon/link.py @@ -0,0 +1,3 @@ +from typing import Dict + +Link = Dict[str, str] diff --git a/falcon/media/base.py b/falcon/media/base.py index ad06b8674..a8edcc6c1 100644 --- a/falcon/media/base.py +++ b/falcon/media/base.py @@ -3,9 +3,10 @@ from typing import IO, Optional, Union from falcon.constants import MEDIA_JSON +from falcon.media_serializer import Serializer -class BaseHandler(metaclass=abc.ABCMeta): +class BaseHandler(Serializer, metaclass=abc.ABCMeta): """Abstract Base Class for an internet media type handler.""" # NOTE(kgriffs): The following special methods are used to enable an diff --git a/falcon/media_serializer.py b/falcon/media_serializer.py new file mode 100644 index 000000000..9cb208280 --- /dev/null +++ b/falcon/media_serializer.py @@ -0,0 +1,11 @@ +from typing import Dict +from typing import Union + +from falcon.link import Link + + +class Serializer: + def serialize( + self, media: Dict[str, Union[str, int, None, Link]], content_type: str + ) -> bytes: + raise NotImplementedError() diff --git a/falcon/middleware.py b/falcon/middleware.py index cecaae1bd..9f83921c6 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -5,13 +5,8 @@ from typing import Optional from typing import Union -from .asgi.request import Request as AsynchronousRequest -from .asgi.response import Response as AsynchronousResponse -from .request import Request as SynchronousRequest -from .response import Response as SynchronousResponse - -Request = Union[AsynchronousRequest, SynchronousRequest] -Response = Union[AsynchronousResponse, SynchronousResponse] +from .request import Request +from .response import Response SynchronousResource = Callable[..., Any] AsynchronousResource = Callable[..., Awaitable[Any]] @@ -137,7 +132,5 @@ def process_response( resp.set_header('Access-Control-Allow-Headers', allow_headers) resp.set_header('Access-Control-Max-Age', '86400') # 24 hours - async def process_response_async( - self, request: Request, response: Response, request_succeeded: bool, *args: Any - ) -> None: + async def process_response_async(self, *args: Any) -> None: self.process_response(*args) From 967d3e858a86d50f1f4a9cc1e38188a5b0d9e5da Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sat, 19 Aug 2023 21:45:09 +0200 Subject: [PATCH 13/65] feat: Add typing to redirects --- falcon/redirects.py | 22 +++++++++++++++++----- pyproject.toml | 1 + 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/falcon/redirects.py b/falcon/redirects.py index e308a5064..92484ca0e 100644 --- a/falcon/redirects.py +++ b/falcon/redirects.py @@ -13,9 +13,11 @@ # limitations under the License. """HTTPStatus specializations for 3xx redirects.""" +from typing import Optional import falcon from falcon.http_status import HTTPStatus +from falcon.typing_http_data import NormalizedHeaders class HTTPMovedPermanently(HTTPStatus): @@ -37,7 +39,9 @@ class HTTPMovedPermanently(HTTPStatus): response. """ - def __init__(self, location, headers=None): + def __init__( + self, location: str, headers: Optional[NormalizedHeaders] = None + ) -> None: if headers is None: headers = {} headers.setdefault('location', location) @@ -66,7 +70,9 @@ class HTTPFound(HTTPStatus): response. """ - def __init__(self, location, headers=None): + def __init__( + self, location: str, headers: Optional[NormalizedHeaders] = None + ) -> None: if headers is None: headers = {} headers.setdefault('location', location) @@ -100,7 +106,9 @@ class HTTPSeeOther(HTTPStatus): response. """ - def __init__(self, location, headers=None): + def __init__( + self, location: str, headers: Optional[NormalizedHeaders] = None + ) -> None: if headers is None: headers = {} headers.setdefault('location', location) @@ -129,7 +137,9 @@ class HTTPTemporaryRedirect(HTTPStatus): response. """ - def __init__(self, location, headers=None): + def __init__( + self, location: str, headers: Optional[NormalizedHeaders] = None + ) -> None: if headers is None: headers = {} headers.setdefault('location', location) @@ -155,7 +165,9 @@ class HTTPPermanentRedirect(HTTPStatus): response. """ - def __init__(self, location, headers=None): + def __init__( + self, location: str, headers: Optional[NormalizedHeaders] = None + ) -> None: if headers is None: headers = {} headers.setdefault('location', location) diff --git a/pyproject.toml b/pyproject.toml index 28dadf85e..17a9249c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ "falcon.http_status", "falcon.inspect", "falcon.middleware", + "falcon.redirects", "falcon.stream", ] disallow_untyped_defs = true From 0d1aa47b8916e6496d324fa14b96ce9b33533624 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sun, 20 Aug 2023 09:00:44 +0200 Subject: [PATCH 14/65] feat: Type vendor mimeparse --- falcon/vendor/mimeparse/mimeparse.py | 35 +++++++++++++++++----------- pyproject.toml | 1 + 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/falcon/vendor/mimeparse/mimeparse.py b/falcon/vendor/mimeparse/mimeparse.py index 0218553cf..6485fcccd 100755 --- a/falcon/vendor/mimeparse/mimeparse.py +++ b/falcon/vendor/mimeparse/mimeparse.py @@ -1,4 +1,9 @@ import cgi +from typing import Dict +from typing import Iterable +from typing import Iterator +from typing import List +from typing import Tuple __version__ = '1.6.0' __author__ = 'Joe Gregorio' @@ -6,12 +11,14 @@ __license__ = 'MIT License' __credits__ = '' +Range = Tuple[str, str, Dict[str, str]] + class MimeTypeParseException(ValueError): pass -def parse_mime_type(mime_type): +def parse_mime_type(mime_type: str) -> Range: """Parses a mime-type into its component parts. Carves up a mime-type and returns a tuple of the (type, subtype, params) @@ -39,7 +46,7 @@ def parse_mime_type(mime_type): return (type.strip(), subtype.strip(), params) -def parse_media_range(range): +def parse_media_range(range: str) -> Range: """Parse a media-range into its component parts. Carves up a media range and returns a tuple of the (type, subtype, @@ -56,7 +63,7 @@ def parse_media_range(range): :rtype: (str,str,dict) """ (type, subtype, params) = parse_mime_type(range) - params.setdefault('q', params.pop('Q', None)) # q is case insensitive + params.setdefault('q', params.pop('Q', '')) # q is case insensitive try: if not params['q'] or not 0 <= float(params['q']) <= 1: params['q'] = '1' @@ -66,7 +73,9 @@ def parse_media_range(range): return (type, subtype, params) -def quality_and_fitness_parsed(mime_type, parsed_ranges): +def quality_and_fitness_parsed( + mime_type: str , parsed_ranges: List[Range] +) -> Tuple[float, float]: """Find the best match for a mime-type amongst parsed media-ranges. Find the best match for a given mime-type against a list of media_ranges @@ -77,8 +86,8 @@ def quality_and_fitness_parsed(mime_type, parsed_ranges): :rtype: (float,int) """ - best_fitness = -1 - best_fit_q = 0 + best_fitness = -1.0 + best_fit_q = 0.0 (target_type, target_subtype, target_params) = \ parse_media_range(mime_type) @@ -98,7 +107,7 @@ def quality_and_fitness_parsed(mime_type, parsed_ranges): if type_match and subtype_match: # 100 points if the type matches w/o a wildcard - fitness = type == target_type and 100 or 0 + fitness: float = type == target_type and 100 or 0 # 10 points if the subtype matches w/o a wildcard fitness += subtype == target_subtype and 10 or 0 @@ -115,12 +124,12 @@ def quality_and_fitness_parsed(mime_type, parsed_ranges): if fitness > best_fitness: best_fitness = fitness - best_fit_q = params['q'] + best_fit_q = float(params['q']) - return float(best_fit_q), best_fitness + return best_fit_q, best_fitness -def quality_parsed(mime_type, parsed_ranges): +def quality_parsed(mime_type: str, parsed_ranges: List[Range]) -> float: """Find the best match for a mime-type amongst parsed media-ranges. Find the best match for a given mime-type against a list of media_ranges @@ -135,7 +144,7 @@ def quality_parsed(mime_type, parsed_ranges): return quality_and_fitness_parsed(mime_type, parsed_ranges)[0] -def quality(mime_type, ranges): +def quality(mime_type: str, ranges: str) -> float: """Return the quality ('q') of a mime-type against a list of media-ranges. Returns the quality 'q' of a mime-type when compared against the @@ -152,7 +161,7 @@ def quality(mime_type, ranges): return quality_parsed(mime_type, parsed_ranges) -def best_match(supported, header): +def best_match(supported: Iterable[str], header: str) -> str: """Return mime-type with the highest quality ('q') from list of candidates. Takes a list of supported mime-types and finds the best match for all the @@ -184,7 +193,7 @@ def best_match(supported, header): return weighted_matches[-1][0][0] and weighted_matches[-1][2] or '' -def _filter_blank(i): +def _filter_blank(i: Iterable[str]) -> Iterator[str]: """Return all non-empty items in the list.""" for s in i: if s.strip(): diff --git a/pyproject.toml b/pyproject.toml index 17a9249c4..dce29752f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ "falcon.middleware", "falcon.redirects", "falcon.stream", + "falcon.vendor.*", ] disallow_untyped_defs = true From 3214966fbce6a175b0d54b2d3128a647d9268c05 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 16:50:27 +0200 Subject: [PATCH 15/65] Changed RawHeaders to not include None --- falcon/errors.py | 76 +++++++++++++++++++------------------- falcon/http_error.py | 2 +- falcon/http_status.py | 2 +- falcon/typing_http_data.py | 2 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index c53c4ea3d..81e62f22c 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -228,7 +228,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ) -> None: super().__init__( @@ -311,7 +311,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, challenges: Optional[Iterable[str]] = None, **kwargs: Kwargs, ): @@ -389,7 +389,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -460,7 +460,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -587,7 +587,7 @@ def __init__( allowed_methods: List[str], title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): headers = _load_headers(headers) @@ -658,7 +658,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -731,7 +731,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -810,7 +810,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -874,7 +874,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -939,7 +939,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1015,7 +1015,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, retry_after: RetryAfter = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ) -> None: super().__init__( @@ -1085,7 +1085,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1150,7 +1150,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1229,7 +1229,7 @@ def __init__( resource_length: int, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): headers = _load_headers(headers) @@ -1299,7 +1299,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1361,7 +1361,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1422,7 +1422,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1491,7 +1491,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1565,7 +1565,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, retry_after: RetryAfter = None, **kwargs: Kwargs, ): @@ -1634,7 +1634,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1709,7 +1709,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1770,7 +1770,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1838,7 +1838,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1899,7 +1899,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -1976,7 +1976,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, retry_after: RetryAfter = None, **kwargs: Kwargs, ): @@ -2039,7 +2039,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -2106,7 +2106,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -2171,7 +2171,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -2233,7 +2233,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -2307,7 +2307,7 @@ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): super().__init__( @@ -2366,7 +2366,7 @@ def __init__( self, msg: str, header_name: str, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): description = 'The value provided for the "{0}" header is invalid. {1}' @@ -2425,7 +2425,7 @@ class HTTPMissingHeader(HTTPBadRequest): def __init__( self, header_name: str, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ): description = 'The "{0}" header is required.' @@ -2488,7 +2488,7 @@ def __init__( self, msg: str, param_name: str, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ) -> None: description = 'The "{0}" parameter is invalid. {1}' @@ -2549,7 +2549,7 @@ class HTTPMissingParam(HTTPBadRequest): def __init__( self, param_name: str, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ) -> None: description = 'The "{0}" parameter is required.' @@ -2715,7 +2715,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, **kwargs: Kwargs, ) -> None: super().__init__( @@ -2763,7 +2763,7 @@ def __init__( # ----------------------------------------------------------------------------- -def _load_headers(headers: RawHeaders) -> NormalizedHeaders: +def _load_headers(headers: Optional[RawHeaders]) -> NormalizedHeaders: """Transform the headers to dict.""" if headers is None: return {} @@ -2773,9 +2773,9 @@ def _load_headers(headers: RawHeaders) -> NormalizedHeaders: def _parse_retry_after( - headers: RawHeaders, + headers: Optional[RawHeaders], retry_after: RetryAfter, -) -> RawHeaders: +) -> Optional[RawHeaders]: """Set the Retry-After to the headers when required.""" if retry_after is None: return headers diff --git a/falcon/http_error.py b/falcon/http_error.py index 46a123ab5..6eb7c253c 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -120,7 +120,7 @@ def __init__( status: Status, title: Optional[str] = None, description: Optional[str] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, href: Optional[str] = None, href_text: Optional[str] = None, code: Optional[int] = None, diff --git a/falcon/http_status.py b/falcon/http_status.py index 2ba89dc13..80fdb24ea 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -50,7 +50,7 @@ class HTTPStatus(Exception): __slots__ = ('status', 'headers', 'text') def __init__( - self, status: Status, headers: RawHeaders = None, text: Optional[str] = None + self, status: Status, headers: Optional[RawHeaders] = None, text: Optional[str] = None ) -> None: self.status = status self.headers = headers diff --git a/falcon/typing_http_data.py b/falcon/typing_http_data.py index 8b207d56b..4bb5fc983 100644 --- a/falcon/typing_http_data.py +++ b/falcon/typing_http_data.py @@ -5,5 +5,5 @@ from typing import Union NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]], None] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] Status = Union[http.HTTPStatus, str, int] From 2c48bc66f5f4f779fd3666b80965a8e636b24dbd Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 16:51:54 +0200 Subject: [PATCH 16/65] Reformated imports --- falcon/typing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/falcon/typing.py b/falcon/typing.py index 2ba96d7be..085b034fd 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -13,7 +13,9 @@ # limitations under the License. """Shorthand definitions for more complex types.""" -from typing import Any, Callable, Pattern +from typing import Any +from typing import Callable +from typing import Pattern from typing import Union from falcon.request import Request From 0368deb6d8419753e80f3af90c8ace03284f38af Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 18:53:12 +0200 Subject: [PATCH 17/65] Test that interface raises not implemented --- .coveragerc | 1 - tests/test_media_serializer.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/test_media_serializer.py diff --git a/.coveragerc b/.coveragerc index 79133debb..2c5041d7e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,4 +13,3 @@ exclude_lines = pragma: nocover pragma: no cover pragma: no py39,py310 cover - raise NotImplementedError() diff --git a/tests/test_media_serializer.py b/tests/test_media_serializer.py new file mode 100644 index 000000000..fa78f742c --- /dev/null +++ b/tests/test_media_serializer.py @@ -0,0 +1,10 @@ +import pytest + +from falcon.media_serializer import Serializer + + +class TestSerializer: + + def test_interface_raises_not_implemented(self) -> None: + with pytest.raises(NotImplementedError): + Serializer().serialize({'data': 'any'}, 'any') From 4e1d1c5141a3933291b658659324f9b1788879f7 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 18:53:50 +0200 Subject: [PATCH 18/65] Type algorithm int values as float --- falcon/vendor/mimeparse/mimeparse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon/vendor/mimeparse/mimeparse.py b/falcon/vendor/mimeparse/mimeparse.py index 6485fcccd..9466b13a5 100755 --- a/falcon/vendor/mimeparse/mimeparse.py +++ b/falcon/vendor/mimeparse/mimeparse.py @@ -86,8 +86,8 @@ def quality_and_fitness_parsed( :rtype: (float,int) """ - best_fitness = -1.0 - best_fit_q = 0.0 + best_fitness: float = -1 + best_fit_q: float = 0 (target_type, target_subtype, target_params) = \ parse_media_range(mime_type) From 328235578a426018f41629977d0109a126264ddc Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 18:56:21 +0200 Subject: [PATCH 19/65] Changed allowed methods to Iterable --- falcon/errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index 81e62f22c..282d80ae6 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -36,7 +36,6 @@ def on_get(self, req, resp): from datetime import datetime from typing import Iterable -from typing import List from typing import Optional from typing import Union @@ -584,7 +583,7 @@ class HTTPMethodNotAllowed(HTTPError): @deprecated_args(allowed_positional=1) def __init__( self, - allowed_methods: List[str], + allowed_methods: Iterable[str], title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, From 9855a97433bbf3b66bb10078ee207285abe499fa Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 18:58:42 +0200 Subject: [PATCH 20/65] Imported annotations in hooks --- falcon/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/falcon/hooks.py b/falcon/hooks.py index 93c84ed2a..021984a25 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -13,6 +13,7 @@ # limitations under the License. """Hook decorators.""" +from __future__ import annotations from functools import wraps from inspect import getmembers From 8709da47eb47220ebf2906ca60422c25d38e3980 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 19:01:29 +0200 Subject: [PATCH 21/65] Change argnames type to list of strings --- falcon/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 021984a25..184a302d7 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -329,7 +329,7 @@ def do_before( def _merge_responder_args( - args: Tuple[Any, ...], kwargs: Dict[str, Any], argnames: List[Any] + args: Tuple[Any, ...], kwargs: Dict[str, Any], argnames: List[str] ) -> None: """Merge responder args into kwargs. From 0e680bda1daa27cf239f52ec8154903646e59909 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 19:03:44 +0200 Subject: [PATCH 22/65] Changed Dict to mutable mapping --- falcon/http_error.py | 5 +++-- falcon/media_serializer.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/falcon/http_error.py b/falcon/http_error.py index 6eb7c253c..b21f118e6 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -15,6 +15,7 @@ """HTTPError exception class.""" from collections import OrderedDict from typing import Dict +from typing import MutableMapping from typing import Optional from typing import Type from typing import Union @@ -156,8 +157,8 @@ def status_code(self) -> int: return http_status_to_code(self.status) def to_dict( - self, obj_type: Type[Dict[str, Union[str, int, None, Link]]] = dict - ) -> Dict[str, Union[str, int, None, Link]]: + self, obj_type: Type[MutableMapping[str, Union[str, int, None, Link]]] = dict + ) -> MutableMapping[str, Union[str, int, None, Link]]: """Return a basic dictionary representing the error. This method can be useful when serializing the error to hash-like diff --git a/falcon/media_serializer.py b/falcon/media_serializer.py index 9cb208280..fd5ff7cb4 100644 --- a/falcon/media_serializer.py +++ b/falcon/media_serializer.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import MutableMapping from typing import Union from falcon.link import Link @@ -6,6 +6,6 @@ class Serializer: def serialize( - self, media: Dict[str, Union[str, int, None, Link]], content_type: str + self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str ) -> bytes: raise NotImplementedError() From 1ec162958d33adea234880c06cc22dc472589f75 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 19:20:18 +0200 Subject: [PATCH 23/65] Fixed formatting --- falcon/http_status.py | 5 ++++- tests/test_media_serializer.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/falcon/http_status.py b/falcon/http_status.py index 80fdb24ea..58329fdd0 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -50,7 +50,10 @@ class HTTPStatus(Exception): __slots__ = ('status', 'headers', 'text') def __init__( - self, status: Status, headers: Optional[RawHeaders] = None, text: Optional[str] = None + self, + status: Status, + headers: Optional[RawHeaders] = None, + text: Optional[str] = None, ) -> None: self.status = status self.headers = headers diff --git a/tests/test_media_serializer.py b/tests/test_media_serializer.py index fa78f742c..797d41849 100644 --- a/tests/test_media_serializer.py +++ b/tests/test_media_serializer.py @@ -4,7 +4,6 @@ class TestSerializer: - def test_interface_raises_not_implemented(self) -> None: with pytest.raises(NotImplementedError): Serializer().serialize({'data': 'any'}, 'any') From a1c8a910f5c298fd1f5032de260682f3f0afea4f Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 19:38:09 +0200 Subject: [PATCH 24/65] Remove unused imports --- falcon/http_error.py | 1 - 1 file changed, 1 deletion(-) diff --git a/falcon/http_error.py b/falcon/http_error.py index b21f118e6..4ac90a4d0 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -14,7 +14,6 @@ """HTTPError exception class.""" from collections import OrderedDict -from typing import Dict from typing import MutableMapping from typing import Optional from typing import Type From 917db59a4a63082d4865db81c674fb47b37577c7 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 19:59:01 +0200 Subject: [PATCH 25/65] Fix typing --- falcon/inspect.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index de8f8ef42..ed5630fd7 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -778,14 +778,16 @@ def visit_app(self, app: AppInfo) -> str: def _get_source_info( - obj: ModuleType - | Type[Any] - | MethodType - | FunctionType - | TracebackType - | FrameType - | CodeType - | Callable[..., Any], + obj: Union[ + ModuleType, + Type[Any], + MethodType, + FunctionType, + TracebackType, + FrameType, + CodeType, + Callable[..., Any], + ], default: Optional[str] = '[unknown file]', ) -> Optional[str]: """Try to get the definition file and line of obj. From 5f9984fdef320fba4c78fd079389403401ee965e Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 20:02:39 +0200 Subject: [PATCH 26/65] Replaced assert with cast --- falcon/inspect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index ed5630fd7..a5f3f6c34 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -32,6 +32,7 @@ from typing import Tuple from typing import Type # NOQA: F401 from typing import Union +from typing import cast from falcon import app_helpers from falcon.app import App @@ -825,7 +826,7 @@ def _get_source_info_and_name( name = getattr(obj, '__name__', None) if name is None: name = getattr(type(obj), '__name__', '[unknown]') - assert name + name = cast(str, name) return source_info, name From edc739ccc96ed201fac870117f1dfe71d50339c6 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 20:49:35 +0200 Subject: [PATCH 27/65] Fix blue --- falcon/inspect.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index a5f3f6c34..17ee8ca11 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -780,14 +780,14 @@ def visit_app(self, app: AppInfo) -> str: def _get_source_info( obj: Union[ - ModuleType, - Type[Any], - MethodType, - FunctionType, - TracebackType, - FrameType, - CodeType, - Callable[..., Any], + ModuleType, + Type[Any], + MethodType, + FunctionType, + TracebackType, + FrameType, + CodeType, + Callable[..., Any], ], default: Optional[str] = '[unknown file]', ) -> Optional[str]: From ae03c3e3825189961181a095bae4583fc8f5bb45 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 20:50:07 +0200 Subject: [PATCH 28/65] Type resource as object --- falcon/middleware.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/falcon/middleware.py b/falcon/middleware.py index 9f83921c6..53d484726 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -8,10 +8,6 @@ from .request import Request from .response import Response -SynchronousResource = Callable[..., Any] -AsynchronousResource = Callable[..., Awaitable[Any]] -Resource = Union[SynchronousResource, AsynchronousResource] - class CORSMiddleware(object): """CORS Middleware. @@ -83,7 +79,7 @@ def __init__( self.allow_credentials = allow_credentials def process_response( - self, req: Request, resp: Response, resource: Resource, req_succeeded: bool + self, req: Request, resp: Response, resource: object, req_succeeded: bool ) -> None: """Implement the CORS policy for all routes. From a66ab366d178f1d63f0f9df955cf5f43b1a87d22 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 24 Aug 2023 21:52:10 +0200 Subject: [PATCH 29/65] Fix style --- falcon/inspect.py | 2 +- falcon/middleware.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index 17ee8ca11..1c24e35dc 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -25,6 +25,7 @@ from types import TracebackType from typing import Any from typing import Callable # NOQA: F401 +from typing import cast from typing import Dict # NOQA: F401 from typing import Iterable from typing import List @@ -32,7 +33,6 @@ from typing import Tuple from typing import Type # NOQA: F401 from typing import Union -from typing import cast from falcon import app_helpers from falcon.app import App diff --git a/falcon/middleware.py b/falcon/middleware.py index 53d484726..87f85d870 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -1,6 +1,4 @@ from typing import Any -from typing import Awaitable -from typing import Callable from typing import Iterable from typing import Optional from typing import Union From 14e9d302f48ed8a0361a376f63f891fb6968f781 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Tue, 29 Aug 2023 22:28:52 +0200 Subject: [PATCH 30/65] Revert "Type algorithm int values as float" This reverts commit ca1df712d95839b78b4930455017bfda402045da. --- falcon/vendor/mimeparse/mimeparse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon/vendor/mimeparse/mimeparse.py b/falcon/vendor/mimeparse/mimeparse.py index 9466b13a5..6485fcccd 100755 --- a/falcon/vendor/mimeparse/mimeparse.py +++ b/falcon/vendor/mimeparse/mimeparse.py @@ -86,8 +86,8 @@ def quality_and_fitness_parsed( :rtype: (float,int) """ - best_fitness: float = -1 - best_fit_q: float = 0 + best_fitness = -1.0 + best_fit_q = 0.0 (target_type, target_subtype, target_params) = \ parse_media_range(mime_type) From 51d0b438ee482ee2744ac1893b75752b59e03a53 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Tue, 29 Aug 2023 22:28:59 +0200 Subject: [PATCH 31/65] Revert "feat: Type vendor mimeparse" This reverts commit 11ca7ca2e9f86e7f021fcf07230f59e67ead3b7e. --- falcon/vendor/mimeparse/mimeparse.py | 35 +++++++++++----------------- pyproject.toml | 1 - 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/falcon/vendor/mimeparse/mimeparse.py b/falcon/vendor/mimeparse/mimeparse.py index 6485fcccd..0218553cf 100755 --- a/falcon/vendor/mimeparse/mimeparse.py +++ b/falcon/vendor/mimeparse/mimeparse.py @@ -1,9 +1,4 @@ import cgi -from typing import Dict -from typing import Iterable -from typing import Iterator -from typing import List -from typing import Tuple __version__ = '1.6.0' __author__ = 'Joe Gregorio' @@ -11,14 +6,12 @@ __license__ = 'MIT License' __credits__ = '' -Range = Tuple[str, str, Dict[str, str]] - class MimeTypeParseException(ValueError): pass -def parse_mime_type(mime_type: str) -> Range: +def parse_mime_type(mime_type): """Parses a mime-type into its component parts. Carves up a mime-type and returns a tuple of the (type, subtype, params) @@ -46,7 +39,7 @@ def parse_mime_type(mime_type: str) -> Range: return (type.strip(), subtype.strip(), params) -def parse_media_range(range: str) -> Range: +def parse_media_range(range): """Parse a media-range into its component parts. Carves up a media range and returns a tuple of the (type, subtype, @@ -63,7 +56,7 @@ def parse_media_range(range: str) -> Range: :rtype: (str,str,dict) """ (type, subtype, params) = parse_mime_type(range) - params.setdefault('q', params.pop('Q', '')) # q is case insensitive + params.setdefault('q', params.pop('Q', None)) # q is case insensitive try: if not params['q'] or not 0 <= float(params['q']) <= 1: params['q'] = '1' @@ -73,9 +66,7 @@ def parse_media_range(range: str) -> Range: return (type, subtype, params) -def quality_and_fitness_parsed( - mime_type: str , parsed_ranges: List[Range] -) -> Tuple[float, float]: +def quality_and_fitness_parsed(mime_type, parsed_ranges): """Find the best match for a mime-type amongst parsed media-ranges. Find the best match for a given mime-type against a list of media_ranges @@ -86,8 +77,8 @@ def quality_and_fitness_parsed( :rtype: (float,int) """ - best_fitness = -1.0 - best_fit_q = 0.0 + best_fitness = -1 + best_fit_q = 0 (target_type, target_subtype, target_params) = \ parse_media_range(mime_type) @@ -107,7 +98,7 @@ def quality_and_fitness_parsed( if type_match and subtype_match: # 100 points if the type matches w/o a wildcard - fitness: float = type == target_type and 100 or 0 + fitness = type == target_type and 100 or 0 # 10 points if the subtype matches w/o a wildcard fitness += subtype == target_subtype and 10 or 0 @@ -124,12 +115,12 @@ def quality_and_fitness_parsed( if fitness > best_fitness: best_fitness = fitness - best_fit_q = float(params['q']) + best_fit_q = params['q'] - return best_fit_q, best_fitness + return float(best_fit_q), best_fitness -def quality_parsed(mime_type: str, parsed_ranges: List[Range]) -> float: +def quality_parsed(mime_type, parsed_ranges): """Find the best match for a mime-type amongst parsed media-ranges. Find the best match for a given mime-type against a list of media_ranges @@ -144,7 +135,7 @@ def quality_parsed(mime_type: str, parsed_ranges: List[Range]) -> float: return quality_and_fitness_parsed(mime_type, parsed_ranges)[0] -def quality(mime_type: str, ranges: str) -> float: +def quality(mime_type, ranges): """Return the quality ('q') of a mime-type against a list of media-ranges. Returns the quality 'q' of a mime-type when compared against the @@ -161,7 +152,7 @@ def quality(mime_type: str, ranges: str) -> float: return quality_parsed(mime_type, parsed_ranges) -def best_match(supported: Iterable[str], header: str) -> str: +def best_match(supported, header): """Return mime-type with the highest quality ('q') from list of candidates. Takes a list of supported mime-types and finds the best match for all the @@ -193,7 +184,7 @@ def best_match(supported: Iterable[str], header: str) -> str: return weighted_matches[-1][0][0] and weighted_matches[-1][2] or '' -def _filter_blank(i: Iterable[str]) -> Iterator[str]: +def _filter_blank(i): """Return all non-empty items in the list.""" for s in i: if s.strip(): diff --git a/pyproject.toml b/pyproject.toml index dce29752f..17a9249c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ "falcon.middleware", "falcon.redirects", "falcon.stream", - "falcon.vendor.*", ] disallow_untyped_defs = true From 73c42b9053efb41f5304ef2eb6ad49ca2feaf596 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 30 Aug 2023 08:51:10 +0200 Subject: [PATCH 32/65] Ignore vendore package --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17a9249c4..53f334f34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,10 @@ ] [tool.mypy] - exclude = "falcon/bench/|falcon/cmd/" + exclude = [ + "falcon/bench/|falcon/cmd/", + "falcon/vendor" + ] [[tool.mypy.overrides]] module = [ "cbor2", From 4a5b2c56b462085345af9498d07c5a56fd02ec03 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 30 Aug 2023 09:29:50 +0200 Subject: [PATCH 33/65] Use async package instead of importing AsyncRequest and AsyncResponse and aliasing them --- falcon/hooks.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 184a302d7..5b0d6dfc1 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -27,11 +27,10 @@ from typing import Tuple from typing import Union -from falcon.asgi.request import Request as AsynchronousRequest -from falcon.asgi.response import Response as AsynchronousResponse +from falcon import asgi from falcon.constants import COMBINED_METHODS -from falcon.request import Request as SynchronousRequest -from falcon.response import Response as SynchronousResponse +from falcon.request import Request +from falcon.response import Response from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe @@ -230,8 +229,8 @@ def _wrap_with_after( @wraps(responder) async def do_after( self: Resource, - req: AsynchronousRequest, - resp: AsynchronousResponse, + req: asgi.Request, + resp: asgi.Response, *args: Any, **kwargs: Any, ) -> None: @@ -247,8 +246,8 @@ async def do_after( @wraps(responder) def do_after( self: Resource, - req: SynchronousRequest, - resp: SynchronousResponse, + req: Request, + resp: Response, *args: Any, **kwargs: Any, ) -> None: @@ -297,8 +296,8 @@ def _wrap_with_before( @wraps(responder) async def do_before( self: Resource, - req: AsynchronousRequest, - resp: AsynchronousResponse, + req: asgi.Request, + resp: asgi.Response, *args: Any, **kwargs: Any, ) -> None: @@ -313,11 +312,7 @@ async def do_before( @wraps(responder) def do_before( - self: Resource, - req: SynchronousRequest, - resp: SynchronousResponse, - *args: Any, - **kwargs: Any, + self: Resource, req: Request, resp: Response, *args: Any, **kwargs: Any ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) From 99f8bb21b1da86c2e4e10ebaf52ae171ec5deccb Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 30 Aug 2023 20:44:49 +0200 Subject: [PATCH 34/65] Solve circular imports while typing --- falcon/errors.py | 7 ++++--- falcon/http_error.py | 8 ++++---- falcon/http_status.py | 4 ++-- falcon/link.py | 3 --- falcon/media/base.py | 2 +- falcon/media/handlers.py | 3 ++- falcon/media_serializer.py | 11 ----------- falcon/redirects.py | 2 +- falcon/request.py | 10 +++++----- falcon/response.py | 18 +++++++++++------- falcon/typing.py | 27 +++++++++++++++++++++++++++ falcon/typing_http_data.py | 9 --------- falcon/util/reader.py | 9 +++++---- tests/test_media_serializer.py | 9 --------- tests/test_typing.py | 17 +++++++++++++++++ 15 files changed, 79 insertions(+), 60 deletions(-) delete mode 100644 falcon/link.py delete mode 100644 falcon/media_serializer.py delete mode 100644 falcon/typing_http_data.py delete mode 100644 tests/test_media_serializer.py create mode 100644 tests/test_typing.py diff --git a/falcon/errors.py b/falcon/errors.py index 282d80ae6..005ce6d1d 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -39,13 +39,14 @@ def on_get(self, req, resp): from typing import Optional from typing import Union -from falcon.http_error import HTTPError import falcon.status_codes as status -from falcon.typing_http_data import NormalizedHeaders -from falcon.typing_http_data import RawHeaders +from falcon.typing import NormalizedHeaders +from falcon.typing import RawHeaders from falcon.util.deprecation import deprecated_args from falcon.util.misc import dt_to_http +from .http_error import HTTPError + __all__ = ( 'CompatibilityError', diff --git a/falcon/http_error.py b/falcon/http_error.py index 4ac90a4d0..5b3f09af7 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -21,10 +21,10 @@ import xml.etree.ElementTree as et from falcon.constants import MEDIA_JSON -from falcon.link import Link -from falcon.media_serializer import Serializer -from falcon.typing_http_data import RawHeaders -from falcon.typing_http_data import Status +from falcon.typing import Link +from falcon.typing import Serializer +from falcon.typing import RawHeaders +from falcon.typing import Status from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args diff --git a/falcon/http_status.py b/falcon/http_status.py index 58329fdd0..2839be8d5 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -15,8 +15,8 @@ """HTTPStatus exception class.""" from typing import Optional -from falcon.typing_http_data import RawHeaders -from falcon.typing_http_data import Status +from falcon.typing import RawHeaders +from falcon.typing import Status from falcon.util import http_status_to_code from falcon.util.deprecation import AttributeRemovedError diff --git a/falcon/link.py b/falcon/link.py deleted file mode 100644 index 51a65db40..000000000 --- a/falcon/link.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Dict - -Link = Dict[str, str] diff --git a/falcon/media/base.py b/falcon/media/base.py index a8edcc6c1..0cb3d2d6a 100644 --- a/falcon/media/base.py +++ b/falcon/media/base.py @@ -3,7 +3,7 @@ from typing import IO, Optional, Union from falcon.constants import MEDIA_JSON -from falcon.media_serializer import Serializer +from falcon.typing import Serializer class BaseHandler(Serializer, metaclass=abc.ABCMeta): diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py index bd3898d3c..d2b8618b6 100644 --- a/falcon/media/handlers.py +++ b/falcon/media/handlers.py @@ -10,6 +10,7 @@ from falcon.media.multipart import MultipartFormHandler from falcon.media.multipart import MultipartParseOptions from falcon.media.urlencoded import URLEncodedFormHandler +from falcon.typing import MediaHandlers from falcon.util import deprecation from falcon.util import misc from falcon.vendor import mimeparse @@ -34,7 +35,7 @@ def _raise(self, *args, **kwargs): serialize = deserialize = _raise -class Handlers(UserDict): +class Handlers(MediaHandlers, UserDict): """A :class:`dict`-like object that manages Internet media type handlers.""" def __init__(self, initial=None): diff --git a/falcon/media_serializer.py b/falcon/media_serializer.py deleted file mode 100644 index fd5ff7cb4..000000000 --- a/falcon/media_serializer.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import MutableMapping -from typing import Union - -from falcon.link import Link - - -class Serializer: - def serialize( - self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str - ) -> bytes: - raise NotImplementedError() diff --git a/falcon/redirects.py b/falcon/redirects.py index 92484ca0e..b13c360b9 100644 --- a/falcon/redirects.py +++ b/falcon/redirects.py @@ -17,7 +17,7 @@ import falcon from falcon.http_status import HTTPStatus -from falcon.typing_http_data import NormalizedHeaders +from falcon.typing import NormalizedHeaders class HTTPMovedPermanently(HTTPStatus): diff --git a/falcon/request.py b/falcon/request.py index cb037369d..9ddc0672c 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -11,9 +11,10 @@ # limitations under the License. """Request class.""" - +from collections import UserDict from datetime import datetime from io import BytesIO +from typing import Dict from uuid import UUID from falcon import errors @@ -26,8 +27,6 @@ # TODO: remove import in falcon 4 from falcon.forwarded import Forwarded # NOQA -from falcon.media import Handlers -from falcon.media.json import _DEFAULT_JSON_HANDLER from falcon.stream import BoundedStream from falcon.util import structures from falcon.util.misc import isascii @@ -1889,6 +1888,7 @@ def get_param_as_json(self, name, required=False, store=None, default=None): MEDIA_JSON, MEDIA_JSON, raise_not_found=False ) if handler is None: + from falcon.media.json import _DEFAULT_JSON_HANDLER # import here to avoid circular imports handler = _DEFAULT_JSON_HANDLER try: @@ -2084,13 +2084,12 @@ class RequestOptions: ``application/json``, ``application/x-www-form-urlencoded`` and ``multipart/form-data`` media types. """ - keep_black_qs_values: bool auto_parse_form_urlencoded: bool auto_parse_qs_csv: bool strip_url_path_trailing_slash: bool default_media_type: str - media_handlers: Handlers + media_handlers: UserDict __slots__ = ( 'keep_blank_qs_values', @@ -2102,6 +2101,7 @@ class RequestOptions: ) def __init__(self): + from falcon.media.handlers import Handlers self.keep_blank_qs_values = True self.auto_parse_form_urlencoded = False self.auto_parse_qs_csv = False diff --git a/falcon/response.py b/falcon/response.py index cad529d3c..cc6b5661f 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -16,19 +16,19 @@ import functools import mimetypes +from collections import UserDict from typing import Optional from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES from falcon.constants import _UNSET from falcon.constants import DEFAULT_MEDIA_TYPE -from falcon.errors import HeaderNotSupported -from falcon.media import Handlers from falcon.response_helpers import format_content_disposition from falcon.response_helpers import format_etag_header from falcon.response_helpers import format_header_value_list from falcon.response_helpers import format_range from falcon.response_helpers import header_property from falcon.response_helpers import is_ascii_encodable +from falcon.typing import MediaHandlers from falcon.util import dt_to_http from falcon.util import http_cookies from falcon.util import http_status_to_code @@ -199,6 +199,9 @@ def __init__(self, options=None): self._media_rendered = _UNSET self.context = self.context_type() + from falcon.errors import HeaderNotSupported + self._header_not_supported = HeaderNotSupported + @property def status_code(self) -> int: @@ -632,7 +635,7 @@ def get_header(self, name, default=None): name = name.lower() if name == 'set-cookie': - raise HeaderNotSupported('Getting Set-Cookie is not currently supported.') + raise self._header_not_supported('Getting Set-Cookie is not currently supported.') return self._headers.get(name, default) @@ -668,7 +671,7 @@ def set_header(self, name, value): name = name.lower() if name == 'set-cookie': - raise HeaderNotSupported('This method cannot be used to set cookies') + raise self._header_not_supported('This method cannot be used to set cookies') self._headers[name] = value @@ -702,7 +705,7 @@ def delete_header(self, name): name = name.lower() if name == 'set-cookie': - raise HeaderNotSupported('This method cannot be used to remove cookies') + raise self._header_not_supported('This method cannot be used to remove cookies') self._headers.pop(name, None) @@ -796,7 +799,7 @@ def set_headers(self, headers): name = name.lower() if name == 'set-cookie': - raise HeaderNotSupported('This method cannot be used to set cookies') + raise self._header_not_supported('This method cannot be used to set cookies') _headers[name] = value @@ -1236,7 +1239,7 @@ class ResponseOptions: secure_cookies_by_default: bool default_media_type: Optional[str] - media_handlers: Handlers + media_handlers: MediaHandlers static_media_types: dict __slots__ = ( @@ -1249,6 +1252,7 @@ class ResponseOptions: def __init__(self): self.secure_cookies_by_default = True self.default_media_type = DEFAULT_MEDIA_TYPE + from falcon.media import Handlers self.media_handlers = Handlers() if not mimetypes.inited: diff --git a/falcon/typing.py b/falcon/typing.py index 085b034fd..1e967115c 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -13,11 +13,34 @@ # limitations under the License. """Shorthand definitions for more complex types.""" +import http from typing import Any from typing import Callable +from typing import Dict +from typing import List +from typing import MutableMapping +from typing import Optional from typing import Pattern +from typing import Tuple from typing import Union + +Link = Dict[str, str] + + +class Serializer: + def serialize( + self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str + ) -> bytes: + raise NotImplementedError() + + +class MediaHandlers: + + def _resolve(self, media_type: str, default: str, raise_not_found: bool = False) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]: + raise NotImplementedError() + + from falcon.request import Request from falcon.response import Response @@ -36,3 +59,7 @@ # arguments afterwords? # class SinkCallable(Protocol): # def __call__(sef, req: Request, resp: Response, ): ... +NormalizedHeaders = Dict[str, str] +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] +Status = Union[http.HTTPStatus, str, int] + diff --git a/falcon/typing_http_data.py b/falcon/typing_http_data.py deleted file mode 100644 index 4bb5fc983..000000000 --- a/falcon/typing_http_data.py +++ /dev/null @@ -1,9 +0,0 @@ -import http -from typing import Dict -from typing import List -from typing import Tuple -from typing import Union - -NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] -Status = Union[http.HTTPStatus, str, int] diff --git a/falcon/util/reader.py b/falcon/util/reader.py index 263d66716..6579d32a4 100644 --- a/falcon/util/reader.py +++ b/falcon/util/reader.py @@ -22,7 +22,6 @@ from typing import List from typing import Optional -from falcon.errors import DelimiterError DEFAULT_CHUNK_SIZE = 32768 """Default chunk size for :class:`BufferedReader` (32 KiB).""" @@ -45,6 +44,8 @@ def __init__( self._buffer_len = 0 self._buffer_pos = 0 self._max_bytes_remaining = max_stream_len + from falcon.errors import DelimiterError + self._delimiter_error = DelimiterError def _perform_read(self, size: int) -> bytes: # PERF(vytas): In Cython, bind types: @@ -216,12 +217,12 @@ def _finalize_read_until( if consume_bytes: if delimiter_pos < 0: if self.peek(consume_bytes) != delimiter: - raise DelimiterError('expected delimiter missing') + raise self._delimiter_error('expected delimiter missing') elif self._buffer_pos != delimiter_pos: # NOTE(vytas): If we are going to consume the delimiter the # quick way (i.e., skipping the above peek() check), we must # make sure it is directly succeeding the result. - raise DelimiterError('expected delimiter missing') + raise self._delimiter_error('expected delimiter missing') self._buffer_pos += consume_bytes @@ -370,7 +371,7 @@ def pipe_until( if consume_delimiter: delimiter_len = len(delimiter) if self.peek(delimiter_len) != delimiter: - raise DelimiterError('expected delimiter missing') + raise self._delimiter_error('expected delimiter missing') self._buffer_pos += delimiter_len def exhaust(self) -> None: diff --git a/tests/test_media_serializer.py b/tests/test_media_serializer.py deleted file mode 100644 index 797d41849..000000000 --- a/tests/test_media_serializer.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest - -from falcon.media_serializer import Serializer - - -class TestSerializer: - def test_interface_raises_not_implemented(self) -> None: - with pytest.raises(NotImplementedError): - Serializer().serialize({'data': 'any'}, 'any') diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 000000000..186a8e123 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,17 @@ +import pytest + +from falcon.typing import MediaHandlers +from falcon.typing import Serializer + + +class TestSerializer: + def test_interface_raises_not_implemented(self) -> None: + with pytest.raises(NotImplementedError): + Serializer().serialize({'data': 'any'}, 'any') + + +class TestMediaHandlers: + + def test_interface_raises_not_implemented(self) -> None: + with pytest.raises(NotImplementedError): + MediaHandlers()._resolve('any', 'any', False) From ee2ff5ef115f2e5bd857ced1de8d67bade05d452 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 30 Aug 2023 20:48:13 +0200 Subject: [PATCH 35/65] Fix style --- falcon/http_error.py | 2 +- falcon/request.py | 8 ++++++-- falcon/response.py | 20 ++++++++++++++------ falcon/typing.py | 10 +++++----- falcon/util/reader.py | 1 + tests/test_typing.py | 1 - 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/falcon/http_error.py b/falcon/http_error.py index 5b3f09af7..9f81457f4 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -22,8 +22,8 @@ from falcon.constants import MEDIA_JSON from falcon.typing import Link -from falcon.typing import Serializer from falcon.typing import RawHeaders +from falcon.typing import Serializer from falcon.typing import Status from falcon.util import code_to_http_status, http_status_to_code, uri from falcon.util.deprecation import deprecated_args diff --git a/falcon/request.py b/falcon/request.py index 9ddc0672c..7e5dcc46b 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -14,7 +14,6 @@ from collections import UserDict from datetime import datetime from io import BytesIO -from typing import Dict from uuid import UUID from falcon import errors @@ -1888,7 +1887,10 @@ def get_param_as_json(self, name, required=False, store=None, default=None): MEDIA_JSON, MEDIA_JSON, raise_not_found=False ) if handler is None: - from falcon.media.json import _DEFAULT_JSON_HANDLER # import here to avoid circular imports + from falcon.media.json import ( + _DEFAULT_JSON_HANDLER, + ) # import here to avoid circular imports + handler = _DEFAULT_JSON_HANDLER try: @@ -2084,6 +2086,7 @@ class RequestOptions: ``application/json``, ``application/x-www-form-urlencoded`` and ``multipart/form-data`` media types. """ + keep_black_qs_values: bool auto_parse_form_urlencoded: bool auto_parse_qs_csv: bool @@ -2102,6 +2105,7 @@ class RequestOptions: def __init__(self): from falcon.media.handlers import Handlers + self.keep_blank_qs_values = True self.auto_parse_form_urlencoded = False self.auto_parse_qs_csv = False diff --git a/falcon/response.py b/falcon/response.py index cc6b5661f..b25a17b84 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -16,7 +16,6 @@ import functools import mimetypes -from collections import UserDict from typing import Optional from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES @@ -200,8 +199,8 @@ def __init__(self, options=None): self.context = self.context_type() from falcon.errors import HeaderNotSupported - self._header_not_supported = HeaderNotSupported + self._header_not_supported = HeaderNotSupported @property def status_code(self) -> int: @@ -635,7 +634,9 @@ def get_header(self, name, default=None): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported('Getting Set-Cookie is not currently supported.') + raise self._header_not_supported( + 'Getting Set-Cookie is not currently supported.' + ) return self._headers.get(name, default) @@ -671,7 +672,9 @@ def set_header(self, name, value): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported('This method cannot be used to set cookies') + raise self._header_not_supported( + 'This method cannot be used to set cookies' + ) self._headers[name] = value @@ -705,7 +708,9 @@ def delete_header(self, name): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported('This method cannot be used to remove cookies') + raise self._header_not_supported( + 'This method cannot be used to remove cookies' + ) self._headers.pop(name, None) @@ -799,7 +804,9 @@ def set_headers(self, headers): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported('This method cannot be used to set cookies') + raise self._header_not_supported( + 'This method cannot be used to set cookies' + ) _headers[name] = value @@ -1253,6 +1260,7 @@ def __init__(self): self.secure_cookies_by_default = True self.default_media_type = DEFAULT_MEDIA_TYPE from falcon.media import Handlers + self.media_handlers = Handlers() if not mimetypes.inited: diff --git a/falcon/typing.py b/falcon/typing.py index 1e967115c..6ba361491 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -36,13 +36,14 @@ def serialize( class MediaHandlers: - - def _resolve(self, media_type: str, default: str, raise_not_found: bool = False) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]: + def _resolve( + self, media_type: str, default: str, raise_not_found: bool = False + ) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]: raise NotImplementedError() -from falcon.request import Request -from falcon.response import Response +from falcon.request import Request # noqa: E402 +from falcon.response import Response # noqa: E402 # Error handlers @@ -62,4 +63,3 @@ def _resolve(self, media_type: str, default: str, raise_not_found: bool = False) NormalizedHeaders = Dict[str, str] RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] Status = Union[http.HTTPStatus, str, int] - diff --git a/falcon/util/reader.py b/falcon/util/reader.py index 6579d32a4..8da340a7e 100644 --- a/falcon/util/reader.py +++ b/falcon/util/reader.py @@ -45,6 +45,7 @@ def __init__( self._buffer_pos = 0 self._max_bytes_remaining = max_stream_len from falcon.errors import DelimiterError + self._delimiter_error = DelimiterError def _perform_read(self, size: int) -> bytes: diff --git a/tests/test_typing.py b/tests/test_typing.py index 186a8e123..417024f64 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -11,7 +11,6 @@ def test_interface_raises_not_implemented(self) -> None: class TestMediaHandlers: - def test_interface_raises_not_implemented(self) -> None: with pytest.raises(NotImplementedError): MediaHandlers()._resolve('any', 'any', False) From 6e02f9a070d39bb495d8414fe62701aeccb1c0c2 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Thu, 31 Aug 2023 10:33:12 +0200 Subject: [PATCH 36/65] Changed inspect obj type to Any --- falcon/inspect.py | 40 +++------------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index 1c24e35dc..fe61d0a71 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -17,12 +17,6 @@ from functools import partial import inspect -from types import CodeType -from types import FrameType -from types import FunctionType -from types import MethodType -from types import ModuleType -from types import TracebackType from typing import Any from typing import Callable # NOQA: F401 from typing import cast @@ -779,17 +773,7 @@ def visit_app(self, app: AppInfo) -> str: def _get_source_info( - obj: Union[ - ModuleType, - Type[Any], - MethodType, - FunctionType, - TracebackType, - FrameType, - CodeType, - Callable[..., Any], - ], - default: Optional[str] = '[unknown file]', + obj: Any, default: Optional[str] = '[unknown file]' ) -> Optional[str]: """Try to get the definition file and line of obj. @@ -808,16 +792,7 @@ def _get_source_info( return source_info -def _get_source_info_and_name( - obj: ModuleType - | Type[Any] - | MethodType - | FunctionType - | TracebackType - | FrameType - | CodeType - | Callable[..., Any] -) -> Tuple[Optional[str], str]: +def _get_source_info_and_name(obj: Any) -> Tuple[Optional[str], str]: """Attempt to get the definition file and line of obj and its name.""" source_info = _get_source_info(obj, None) if source_info is None: @@ -830,16 +805,7 @@ def _get_source_info_and_name( return source_info, name -def _is_internal( - obj: ModuleType - | Type[Any] - | MethodType - | FunctionType - | TracebackType - | FrameType - | CodeType - | Callable[..., Any] -) -> bool: +def _is_internal(obj: Any) -> bool: """Check if the module of the object is a falcon module.""" module = inspect.getmodule(obj) if module: From f7bd33c46d1fa46892fb209001090dab8cecdd95 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 4 Sep 2023 22:17:57 +0200 Subject: [PATCH 37/65] Import annotations where missing --- falcon/asgi_spec.py | 1 + falcon/errors.py | 1 + falcon/forwarded.py | 1 + falcon/http_error.py | 1 + falcon/http_status.py | 1 + falcon/middleware.py | 2 ++ falcon/redirects.py | 1 + falcon/typing.py | 1 + 8 files changed, 9 insertions(+) diff --git a/falcon/asgi_spec.py b/falcon/asgi_spec.py index 5e80a7241..a3b38efa6 100644 --- a/falcon/asgi_spec.py +++ b/falcon/asgi_spec.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Constants, etc. defined by the ASGI specification.""" diff --git a/falcon/errors.py b/falcon/errors.py index 005ce6d1d..2cef973bd 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """HTTP error classes and other Falcon-specific errors. diff --git a/falcon/forwarded.py b/falcon/forwarded.py index 64bb32dc7..39b972990 100644 --- a/falcon/forwarded.py +++ b/falcon/forwarded.py @@ -16,6 +16,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations import re import string diff --git a/falcon/http_error.py b/falcon/http_error.py index 9f81457f4..308b35c18 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """HTTPError exception class.""" from collections import OrderedDict diff --git a/falcon/http_status.py b/falcon/http_status.py index 2839be8d5..1e4a139bb 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """HTTPStatus exception class.""" from typing import Optional diff --git a/falcon/middleware.py b/falcon/middleware.py index 87f85d870..0bb043dbb 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any from typing import Iterable from typing import Optional diff --git a/falcon/redirects.py b/falcon/redirects.py index b13c360b9..91f309fd2 100644 --- a/falcon/redirects.py +++ b/falcon/redirects.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """HTTPStatus specializations for 3xx redirects.""" from typing import Optional diff --git a/falcon/typing.py b/falcon/typing.py index 6ba361491..0f68c6ddc 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations """Shorthand definitions for more complex types.""" import http From 7470daebcd75ab98de065d59cd5bf26da7a8a12b Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 6 Sep 2023 11:49:01 +0200 Subject: [PATCH 38/65] Replace Union with | where future annotations imported --- falcon/errors.py | 9 ++++----- falcon/hooks.py | 5 ++--- falcon/http_error.py | 5 ++--- falcon/inspect.py | 13 +++++-------- falcon/middleware.py | 7 +++---- falcon/stream.py | 4 ++-- falcon/typing.py | 9 ++++----- 7 files changed, 22 insertions(+), 30 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index 2cef973bd..8c9826b5d 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -38,7 +38,6 @@ def on_get(self, req, resp): from datetime import datetime from typing import Iterable from typing import Optional -from typing import Union import falcon.status_codes as status from falcon.typing import NormalizedHeaders @@ -175,8 +174,8 @@ class WebSocketServerError(WebSocketDisconnected): pass -Kwargs = Union[str, int, None] -RetryAfter = Union[int, datetime, None] +Kwargs = str | int | None +RetryAfter = int | datetime | None class HTTPBadRequest(HTTPError): @@ -2648,7 +2647,7 @@ class MediaMalformedError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type: str, **kwargs: Union[RawHeaders, Kwargs]): + def __init__(self, media_type: str, **kwargs: RawHeaders | Kwargs): super().__init__( title='Invalid {0}'.format(media_type), description=None, **kwargs ) @@ -2749,7 +2748,7 @@ class MultipartParseError(MediaMalformedError): @deprecated_args(allowed_positional=0) def __init__( - self, description: Optional[str] = None, **kwargs: Union[RawHeaders, Kwargs] + self, description: Optional[str] = None, **kwargs: RawHeaders | Kwargs ) -> None: HTTPBadRequest.__init__( self, diff --git a/falcon/hooks.py b/falcon/hooks.py index 5b0d6dfc1..c7f44f047 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -25,7 +25,6 @@ from typing import Dict from typing import List from typing import Tuple -from typing import Union from falcon import asgi from falcon.constants import COMBINED_METHODS @@ -41,7 +40,7 @@ SynchronousResource = Callable[..., Any] AsynchronousResource = Callable[..., Awaitable[Any]] -Resource = Union[SynchronousResource, AsynchronousResource] +Resource = SynchronousResource | AsynchronousResource def before( @@ -266,7 +265,7 @@ def _wrap_with_before( action_args: Tuple[Any, ...], action_kwargs: Dict[str, Any], is_async: bool, -) -> Union[Callable[..., Awaitable[None]], Callable[..., None]]: +) -> Callable[..., Awaitable[None]] | Callable[..., None]: """Execute the given action function before a responder method. Args: diff --git a/falcon/http_error.py b/falcon/http_error.py index 308b35c18..2d5a82ed8 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -18,7 +18,6 @@ from typing import MutableMapping from typing import Optional from typing import Type -from typing import Union import xml.etree.ElementTree as et from falcon.constants import MEDIA_JSON @@ -157,8 +156,8 @@ def status_code(self) -> int: return http_status_to_code(self.status) def to_dict( - self, obj_type: Type[MutableMapping[str, Union[str, int, None, Link]]] = dict - ) -> MutableMapping[str, Union[str, int, None, Link]]: + self, obj_type: Type[MutableMapping[str, str | int | None | Link]] = dict + ) -> MutableMapping[str, str | int | None | Link]: """Return a basic dictionary representing the error. This method can be useful when serializing the error to hash-like diff --git a/falcon/inspect.py b/falcon/inspect.py index fe61d0a71..fc338b6ca 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -26,7 +26,6 @@ from typing import Optional from typing import Tuple from typing import Type # NOQA: F401 -from typing import Union from falcon import app_helpers from falcon.app import App @@ -615,7 +614,7 @@ def visit_route_method(self, route_method: RouteMethodInfo) -> str: return text def _methods_to_string( - self, methods: Union[List[RouteMethodInfo], List[MiddlewareMethodInfo]] + self, methods: List[RouteMethodInfo] | List[MiddlewareMethodInfo] ) -> str: """Return a string from the list of methods.""" tab = self.tab + ' ' * 3 @@ -814,13 +813,11 @@ def _is_internal(obj: Any) -> bool: def _filter_internal( - iterable: Union[ - Iterable[RouteMethodInfo], - Iterable[ErrorHandlerInfo], - Iterable[MiddlewareMethodInfo], - ], + iterable: Iterable[RouteMethodInfo] + | Iterable[ErrorHandlerInfo] + | Iterable[MiddlewareMethodInfo], return_internal: bool, -) -> Union[Iterable[_Traversable], List[_Traversable]]: +) -> Iterable[_Traversable] | List[_Traversable]: """Filter the internal elements of an iterable.""" if return_internal: return iterable diff --git a/falcon/middleware.py b/falcon/middleware.py index 0bb043dbb..19558db9d 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -3,7 +3,6 @@ from typing import Any from typing import Iterable from typing import Optional -from typing import Union from .request import Request from .response import Response @@ -45,9 +44,9 @@ class CORSMiddleware(object): def __init__( self, - allow_origins: Union[str, Iterable[str]] = '*', - expose_headers: Optional[Union[str, Iterable[str]]] = None, - allow_credentials: Optional[Union[str, Iterable[str]]] = None, + allow_origins: str | Iterable[str] = '*', + expose_headers: Optional[str | Iterable[str]] = None, + allow_credentials: Optional[str | Iterable[str]] = None, ): if allow_origins == '*': self.allow_origins = allow_origins diff --git a/falcon/stream.py b/falcon/stream.py index c300ee98a..387fbcd41 100644 --- a/falcon/stream.py +++ b/falcon/stream.py @@ -17,12 +17,12 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable, List, Optional, TypeVar, Union +from typing import BinaryIO, Callable, List, Optional, TypeVar __all__ = ['BoundedStream'] -Result = TypeVar('Result', bound=Union[bytes, List[bytes]]) +Result = TypeVar('Result', bound=bytes | List[bytes]) class BoundedStream(io.IOBase): diff --git a/falcon/typing.py b/falcon/typing.py index 0f68c6ddc..5410a7ecf 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -23,7 +23,6 @@ from typing import Optional from typing import Pattern from typing import Tuple -from typing import Union Link = Dict[str, str] @@ -31,7 +30,7 @@ class Serializer: def serialize( - self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str + self, media: MutableMapping[str, str | int | None | Link], content_type: str ) -> bytes: raise NotImplementedError() @@ -54,7 +53,7 @@ def _resolve( ErrorSerializer = Callable[[Request, Response, BaseException], Any] # Sinks -SinkPrefix = Union[str, Pattern] +SinkPrefix = str | Pattern # TODO(vytas): Is it possible to specify a Callable or a Protocol that defines # type hints for the two first parameters, but accepts any number of keyword @@ -62,5 +61,5 @@ def _resolve( # class SinkCallable(Protocol): # def __call__(sef, req: Request, resp: Response, ): ... NormalizedHeaders = Dict[str, str] -RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] -Status = Union[http.HTTPStatus, str, int] +RawHeaders = NormalizedHeaders | List[Tuple[str, str]] +Status = http.HTTPStatus | str | int From e44512e11f307f5eed3bfcf140d7c425fcb58020 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Wed, 6 Sep 2023 20:40:46 +0200 Subject: [PATCH 39/65] Revert "Replace Union with | where future annotations imported" This reverts commit fd8b3be07ff9471bc774cf17f096092708ec35e6. --- falcon/errors.py | 9 +++++---- falcon/hooks.py | 5 +++-- falcon/http_error.py | 5 +++-- falcon/inspect.py | 13 ++++++++----- falcon/middleware.py | 7 ++++--- falcon/stream.py | 4 ++-- falcon/typing.py | 9 +++++---- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index 8c9826b5d..2cef973bd 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -38,6 +38,7 @@ def on_get(self, req, resp): from datetime import datetime from typing import Iterable from typing import Optional +from typing import Union import falcon.status_codes as status from falcon.typing import NormalizedHeaders @@ -174,8 +175,8 @@ class WebSocketServerError(WebSocketDisconnected): pass -Kwargs = str | int | None -RetryAfter = int | datetime | None +Kwargs = Union[str, int, None] +RetryAfter = Union[int, datetime, None] class HTTPBadRequest(HTTPError): @@ -2647,7 +2648,7 @@ class MediaMalformedError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type: str, **kwargs: RawHeaders | Kwargs): + def __init__(self, media_type: str, **kwargs: Union[RawHeaders, Kwargs]): super().__init__( title='Invalid {0}'.format(media_type), description=None, **kwargs ) @@ -2748,7 +2749,7 @@ class MultipartParseError(MediaMalformedError): @deprecated_args(allowed_positional=0) def __init__( - self, description: Optional[str] = None, **kwargs: RawHeaders | Kwargs + self, description: Optional[str] = None, **kwargs: Union[RawHeaders, Kwargs] ) -> None: HTTPBadRequest.__init__( self, diff --git a/falcon/hooks.py b/falcon/hooks.py index c7f44f047..5b0d6dfc1 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -25,6 +25,7 @@ from typing import Dict from typing import List from typing import Tuple +from typing import Union from falcon import asgi from falcon.constants import COMBINED_METHODS @@ -40,7 +41,7 @@ SynchronousResource = Callable[..., Any] AsynchronousResource = Callable[..., Awaitable[Any]] -Resource = SynchronousResource | AsynchronousResource +Resource = Union[SynchronousResource, AsynchronousResource] def before( @@ -265,7 +266,7 @@ def _wrap_with_before( action_args: Tuple[Any, ...], action_kwargs: Dict[str, Any], is_async: bool, -) -> Callable[..., Awaitable[None]] | Callable[..., None]: +) -> Union[Callable[..., Awaitable[None]], Callable[..., None]]: """Execute the given action function before a responder method. Args: diff --git a/falcon/http_error.py b/falcon/http_error.py index 2d5a82ed8..308b35c18 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -18,6 +18,7 @@ from typing import MutableMapping from typing import Optional from typing import Type +from typing import Union import xml.etree.ElementTree as et from falcon.constants import MEDIA_JSON @@ -156,8 +157,8 @@ def status_code(self) -> int: return http_status_to_code(self.status) def to_dict( - self, obj_type: Type[MutableMapping[str, str | int | None | Link]] = dict - ) -> MutableMapping[str, str | int | None | Link]: + self, obj_type: Type[MutableMapping[str, Union[str, int, None, Link]]] = dict + ) -> MutableMapping[str, Union[str, int, None, Link]]: """Return a basic dictionary representing the error. This method can be useful when serializing the error to hash-like diff --git a/falcon/inspect.py b/falcon/inspect.py index fc338b6ca..fe61d0a71 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -26,6 +26,7 @@ from typing import Optional from typing import Tuple from typing import Type # NOQA: F401 +from typing import Union from falcon import app_helpers from falcon.app import App @@ -614,7 +615,7 @@ def visit_route_method(self, route_method: RouteMethodInfo) -> str: return text def _methods_to_string( - self, methods: List[RouteMethodInfo] | List[MiddlewareMethodInfo] + self, methods: Union[List[RouteMethodInfo], List[MiddlewareMethodInfo]] ) -> str: """Return a string from the list of methods.""" tab = self.tab + ' ' * 3 @@ -813,11 +814,13 @@ def _is_internal(obj: Any) -> bool: def _filter_internal( - iterable: Iterable[RouteMethodInfo] - | Iterable[ErrorHandlerInfo] - | Iterable[MiddlewareMethodInfo], + iterable: Union[ + Iterable[RouteMethodInfo], + Iterable[ErrorHandlerInfo], + Iterable[MiddlewareMethodInfo], + ], return_internal: bool, -) -> Iterable[_Traversable] | List[_Traversable]: +) -> Union[Iterable[_Traversable], List[_Traversable]]: """Filter the internal elements of an iterable.""" if return_internal: return iterable diff --git a/falcon/middleware.py b/falcon/middleware.py index 19558db9d..0bb043dbb 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -3,6 +3,7 @@ from typing import Any from typing import Iterable from typing import Optional +from typing import Union from .request import Request from .response import Response @@ -44,9 +45,9 @@ class CORSMiddleware(object): def __init__( self, - allow_origins: str | Iterable[str] = '*', - expose_headers: Optional[str | Iterable[str]] = None, - allow_credentials: Optional[str | Iterable[str]] = None, + allow_origins: Union[str, Iterable[str]] = '*', + expose_headers: Optional[Union[str, Iterable[str]]] = None, + allow_credentials: Optional[Union[str, Iterable[str]]] = None, ): if allow_origins == '*': self.allow_origins = allow_origins diff --git a/falcon/stream.py b/falcon/stream.py index 387fbcd41..c300ee98a 100644 --- a/falcon/stream.py +++ b/falcon/stream.py @@ -17,12 +17,12 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable, List, Optional, TypeVar +from typing import BinaryIO, Callable, List, Optional, TypeVar, Union __all__ = ['BoundedStream'] -Result = TypeVar('Result', bound=bytes | List[bytes]) +Result = TypeVar('Result', bound=Union[bytes, List[bytes]]) class BoundedStream(io.IOBase): diff --git a/falcon/typing.py b/falcon/typing.py index 5410a7ecf..0f68c6ddc 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -23,6 +23,7 @@ from typing import Optional from typing import Pattern from typing import Tuple +from typing import Union Link = Dict[str, str] @@ -30,7 +31,7 @@ class Serializer: def serialize( - self, media: MutableMapping[str, str | int | None | Link], content_type: str + self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str ) -> bytes: raise NotImplementedError() @@ -53,7 +54,7 @@ def _resolve( ErrorSerializer = Callable[[Request, Response, BaseException], Any] # Sinks -SinkPrefix = str | Pattern +SinkPrefix = Union[str, Pattern] # TODO(vytas): Is it possible to specify a Callable or a Protocol that defines # type hints for the two first parameters, but accepts any number of keyword @@ -61,5 +62,5 @@ def _resolve( # class SinkCallable(Protocol): # def __call__(sef, req: Request, resp: Response, ): ... NormalizedHeaders = Dict[str, str] -RawHeaders = NormalizedHeaders | List[Tuple[str, str]] -Status = http.HTTPStatus | str | int +RawHeaders = Union[NormalizedHeaders, List[Tuple[str, str]]] +Status = Union[http.HTTPStatus, str, int] From d5a87c5978c72794cb452e5046780537699e15f1 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Sat, 9 Sep 2023 22:23:41 +0200 Subject: [PATCH 40/65] Improve imports to avoid them inside functions --- falcon/asgi_spec.py | 2 +- falcon/errors.py | 10 ++++++---- falcon/hooks.py | 17 +++++++++++------ falcon/http_error.py | 23 +++++++++++++++-------- falcon/http_status.py | 9 ++++++--- falcon/redirects.py | 7 +++++-- falcon/request.py | 10 ++++------ falcon/response.py | 28 ++++++++++------------------ falcon/typing.py | 14 +++++++------- falcon/util/reader.py | 10 ++++------ 10 files changed, 69 insertions(+), 61 deletions(-) diff --git a/falcon/asgi_spec.py b/falcon/asgi_spec.py index a3b38efa6..02f214dec 100644 --- a/falcon/asgi_spec.py +++ b/falcon/asgi_spec.py @@ -11,9 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations """Constants, etc. defined by the ASGI specification.""" +from __future__ import annotations class EventType: diff --git a/falcon/errors.py b/falcon/errors.py index 2cef973bd..463a0824c 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations """HTTP error classes and other Falcon-specific errors. @@ -34,19 +33,22 @@ def on_get(self, req, resp): # -- snip -- """ +from __future__ import annotations from datetime import datetime from typing import Iterable from typing import Optional +from typing import TYPE_CHECKING from typing import Union +from falcon.http_error import HTTPError import falcon.status_codes as status -from falcon.typing import NormalizedHeaders -from falcon.typing import RawHeaders from falcon.util.deprecation import deprecated_args from falcon.util.misc import dt_to_http -from .http_error import HTTPError +if TYPE_CHECKING: + from falcon.typing import NormalizedHeaders + from falcon.typing import RawHeaders __all__ = ( diff --git a/falcon/hooks.py b/falcon/hooks.py index 5b0d6dfc1..f994ec01e 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -25,15 +25,16 @@ from typing import Dict from typing import List from typing import Tuple +from typing import TYPE_CHECKING from typing import Union -from falcon import asgi from falcon.constants import COMBINED_METHODS -from falcon.request import Request -from falcon.response import Response from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe +if TYPE_CHECKING: + import falcon as wsgi + from falcon import asgi _DECORABLE_METHOD_NAME = re.compile( r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS)) @@ -246,8 +247,8 @@ async def do_after( @wraps(responder) def do_after( self: Resource, - req: Request, - resp: Response, + req: wsgi.Request, + resp: wsgi.Response, *args: Any, **kwargs: Any, ) -> None: @@ -312,7 +313,11 @@ async def do_before( @wraps(responder) def do_before( - self: Resource, req: Request, resp: Response, *args: Any, **kwargs: Any + self: Resource, + req: wsgi.Request, + resp: wsgi.Response, + *args: Any, + **kwargs: Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) diff --git a/falcon/http_error.py b/falcon/http_error.py index 308b35c18..16a1c8ef4 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -11,24 +11,29 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""HTTPError exception class.""" from __future__ import annotations -"""HTTPError exception class.""" from collections import OrderedDict from typing import MutableMapping from typing import Optional from typing import Type +from typing import TYPE_CHECKING from typing import Union import xml.etree.ElementTree as et from falcon.constants import MEDIA_JSON -from falcon.typing import Link -from falcon.typing import RawHeaders -from falcon.typing import Serializer -from falcon.typing import Status -from falcon.util import code_to_http_status, http_status_to_code, uri +from falcon.util import code_to_http_status +from falcon.util import http_status_to_code +from falcon.util import uri from falcon.util.deprecation import deprecated_args +if TYPE_CHECKING: + from falcon.typing import Link + from falcon.typing import RawHeaders + from falcon.typing import Serializer + from falcon.typing import Status + class HTTPError(Exception): """Represents a generic HTTP error. @@ -205,7 +210,6 @@ def to_json(self, handler: Optional[Serializer] = None) -> bytes: obj = self.to_dict(OrderedDict) if handler is None: handler = _DEFAULT_JSON_HANDLER - assert handler return handler.serialize(obj, MEDIA_JSON) def to_xml(self) -> bytes: @@ -239,4 +243,7 @@ def to_xml(self) -> bytes: # NOTE: initialized in falcon.media.json, that is always imported since Request/Response # are imported by falcon init. -_DEFAULT_JSON_HANDLER = None +if TYPE_CHECKING: + _DEFAULT_JSON_HANDLER: Serializer +else: + _DEFAULT_JSON_HANDLER = None diff --git a/falcon/http_status.py b/falcon/http_status.py index 1e4a139bb..54e1ebac0 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -11,16 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""HTTPStatus exception class.""" from __future__ import annotations -"""HTTPStatus exception class.""" from typing import Optional +from typing import TYPE_CHECKING -from falcon.typing import RawHeaders -from falcon.typing import Status from falcon.util import http_status_to_code from falcon.util.deprecation import AttributeRemovedError +if TYPE_CHECKING: + from falcon.typing import RawHeaders + from falcon.typing import Status + class HTTPStatus(Exception): """Represents a generic HTTP status. diff --git a/falcon/redirects.py b/falcon/redirects.py index 91f309fd2..3f2e94760 100644 --- a/falcon/redirects.py +++ b/falcon/redirects.py @@ -11,14 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""HTTPStatus specializations for 3xx redirects.""" from __future__ import annotations -"""HTTPStatus specializations for 3xx redirects.""" from typing import Optional +from typing import TYPE_CHECKING import falcon from falcon.http_status import HTTPStatus -from falcon.typing import NormalizedHeaders + +if TYPE_CHECKING: + from falcon.typing import NormalizedHeaders class HTTPMovedPermanently(HTTPStatus): diff --git a/falcon/request.py b/falcon/request.py index 7e5dcc46b..f20b2c3d5 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -11,6 +11,8 @@ # limitations under the License. """Request class.""" +from __future__ import annotations + from collections import UserDict from datetime import datetime from io import BytesIO @@ -26,6 +28,8 @@ # TODO: remove import in falcon 4 from falcon.forwarded import Forwarded # NOQA +from falcon.media import Handlers +from falcon.media.json import _DEFAULT_JSON_HANDLER from falcon.stream import BoundedStream from falcon.util import structures from falcon.util.misc import isascii @@ -1887,10 +1891,6 @@ def get_param_as_json(self, name, required=False, store=None, default=None): MEDIA_JSON, MEDIA_JSON, raise_not_found=False ) if handler is None: - from falcon.media.json import ( - _DEFAULT_JSON_HANDLER, - ) # import here to avoid circular imports - handler = _DEFAULT_JSON_HANDLER try: @@ -2104,8 +2104,6 @@ class RequestOptions: ) def __init__(self): - from falcon.media.handlers import Handlers - self.keep_blank_qs_values = True self.auto_parse_form_urlencoded = False self.auto_parse_qs_csv = False diff --git a/falcon/response.py b/falcon/response.py index b25a17b84..edb713a81 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -13,21 +13,24 @@ # limitations under the License. """Response class.""" +from __future__ import annotations import functools import mimetypes from typing import Optional +from typing import TYPE_CHECKING from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES from falcon.constants import _UNSET from falcon.constants import DEFAULT_MEDIA_TYPE +from falcon.errors import HeaderNotSupported +from falcon.media import Handlers from falcon.response_helpers import format_content_disposition from falcon.response_helpers import format_etag_header from falcon.response_helpers import format_header_value_list from falcon.response_helpers import format_range from falcon.response_helpers import header_property from falcon.response_helpers import is_ascii_encodable -from falcon.typing import MediaHandlers from falcon.util import dt_to_http from falcon.util import http_cookies from falcon.util import http_status_to_code @@ -37,6 +40,8 @@ from falcon.util.uri import encode_check_escaped as uri_encode from falcon.util.uri import encode_value_check_escaped as uri_encode_value +if TYPE_CHECKING: + from falcon.typing import MediaHandlers GMT_TIMEZONE = TimezoneGMT() @@ -198,9 +203,6 @@ def __init__(self, options=None): self._media_rendered = _UNSET self.context = self.context_type() - from falcon.errors import HeaderNotSupported - - self._header_not_supported = HeaderNotSupported @property def status_code(self) -> int: @@ -634,9 +636,7 @@ def get_header(self, name, default=None): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported( - 'Getting Set-Cookie is not currently supported.' - ) + raise HeaderNotSupported('Getting Set-Cookie is not currently supported.') return self._headers.get(name, default) @@ -672,9 +672,7 @@ def set_header(self, name, value): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported( - 'This method cannot be used to set cookies' - ) + raise HeaderNotSupported('This method cannot be used to set cookies') self._headers[name] = value @@ -708,9 +706,7 @@ def delete_header(self, name): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported( - 'This method cannot be used to remove cookies' - ) + raise HeaderNotSupported('This method cannot be used to remove ookies') self._headers.pop(name, None) @@ -804,9 +800,7 @@ def set_headers(self, headers): name = name.lower() if name == 'set-cookie': - raise self._header_not_supported( - 'This method cannot be used to set cookies' - ) + raise HeaderNotSupported('This method cannot be used to set cookies') _headers[name] = value @@ -1259,8 +1253,6 @@ class ResponseOptions: def __init__(self): self.secure_cookies_by_default = True self.default_media_type = DEFAULT_MEDIA_TYPE - from falcon.media import Handlers - self.media_handlers = Handlers() if not mimetypes.inited: diff --git a/falcon/typing.py b/falcon/typing.py index 0f68c6ddc..645fd6a87 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -11,9 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Shorthand definitions for more complex types.""" from __future__ import annotations -"""Shorthand definitions for more complex types.""" import http from typing import Any from typing import Callable @@ -23,8 +23,12 @@ from typing import Optional from typing import Pattern from typing import Tuple +from typing import TYPE_CHECKING from typing import Union +if TYPE_CHECKING: + from falcon.request import Request + from falcon.response import Response Link = Dict[str, str] @@ -43,15 +47,11 @@ def _resolve( raise NotImplementedError() -from falcon.request import Request # noqa: E402 -from falcon.response import Response # noqa: E402 - - # Error handlers -ErrorHandler = Callable[[Request, Response, BaseException, dict], Any] +ErrorHandler = Callable[['Request', 'Response', BaseException, dict], Any] # Error serializers -ErrorSerializer = Callable[[Request, Response, BaseException], Any] +ErrorSerializer = Callable[['Request', 'Response', BaseException], Any] # Sinks SinkPrefix = Union[str, Pattern] diff --git a/falcon/util/reader.py b/falcon/util/reader.py index 8da340a7e..263d66716 100644 --- a/falcon/util/reader.py +++ b/falcon/util/reader.py @@ -22,6 +22,7 @@ from typing import List from typing import Optional +from falcon.errors import DelimiterError DEFAULT_CHUNK_SIZE = 32768 """Default chunk size for :class:`BufferedReader` (32 KiB).""" @@ -44,9 +45,6 @@ def __init__( self._buffer_len = 0 self._buffer_pos = 0 self._max_bytes_remaining = max_stream_len - from falcon.errors import DelimiterError - - self._delimiter_error = DelimiterError def _perform_read(self, size: int) -> bytes: # PERF(vytas): In Cython, bind types: @@ -218,12 +216,12 @@ def _finalize_read_until( if consume_bytes: if delimiter_pos < 0: if self.peek(consume_bytes) != delimiter: - raise self._delimiter_error('expected delimiter missing') + raise DelimiterError('expected delimiter missing') elif self._buffer_pos != delimiter_pos: # NOTE(vytas): If we are going to consume the delimiter the # quick way (i.e., skipping the above peek() check), we must # make sure it is directly succeeding the result. - raise self._delimiter_error('expected delimiter missing') + raise DelimiterError('expected delimiter missing') self._buffer_pos += consume_bytes @@ -372,7 +370,7 @@ def pipe_until( if consume_delimiter: delimiter_len = len(delimiter) if self.peek(delimiter_len) != delimiter: - raise self._delimiter_error('expected delimiter missing') + raise DelimiterError('expected delimiter missing') self._buffer_pos += delimiter_len def exhaust(self) -> None: From 0f9a0c30fe518014ad3fc80eb2389829f8d0a9dd Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 16 Oct 2023 21:18:48 +0200 Subject: [PATCH 41/65] Fix typo --- falcon/errors.py | 78 ++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index 463a0824c..41f22238d 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -177,7 +177,7 @@ class WebSocketServerError(WebSocketDisconnected): pass -Kwargs = Union[str, int, None] +HTTPErrorKeywordArguments = Union[str, int, None] RetryAfter = Union[int, datetime, None] @@ -232,7 +232,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( status.HTTP_400, @@ -316,7 +316,7 @@ def __init__( description: Optional[str] = None, headers: Optional[RawHeaders] = None, challenges: Optional[Iterable[str]] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): if challenges: headers = _load_headers(headers) @@ -393,7 +393,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_403, @@ -464,7 +464,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_404, @@ -591,7 +591,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) headers['Allow'] = ', '.join(allowed_methods) @@ -662,7 +662,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_406, @@ -735,7 +735,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_409, @@ -814,7 +814,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_410, @@ -878,7 +878,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_411, @@ -943,7 +943,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_412, @@ -1019,7 +1019,7 @@ def __init__( description: Optional[str] = None, retry_after: RetryAfter = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( status.HTTP_413, @@ -1089,7 +1089,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_414, @@ -1154,7 +1154,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_415, @@ -1233,7 +1233,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) headers['Content-Range'] = 'bytes */' + str(resource_length) @@ -1303,7 +1303,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_422, @@ -1365,7 +1365,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_423, @@ -1426,7 +1426,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_424, @@ -1495,7 +1495,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_428, @@ -1570,7 +1570,7 @@ def __init__( description: Optional[str] = None, headers: Optional[RawHeaders] = None, retry_after: RetryAfter = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_429, @@ -1638,7 +1638,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_431, @@ -1713,7 +1713,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_451, @@ -1774,7 +1774,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_500, @@ -1842,7 +1842,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_501, @@ -1903,7 +1903,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_502, @@ -1981,7 +1981,7 @@ def __init__( description: Optional[str] = None, headers: Optional[RawHeaders] = None, retry_after: RetryAfter = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_503, @@ -2043,7 +2043,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_504, @@ -2110,7 +2110,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_505, @@ -2175,7 +2175,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_507, @@ -2237,7 +2237,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_508, @@ -2311,7 +2311,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): super().__init__( status.HTTP_511, @@ -2370,7 +2370,7 @@ def __init__( msg: str, header_name: str, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): description = 'The value provided for the "{0}" header is invalid. {1}' description = description.format(header_name, msg) @@ -2429,7 +2429,7 @@ def __init__( self, header_name: str, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ): description = 'The "{0}" header is required.' description = description.format(header_name) @@ -2492,7 +2492,7 @@ def __init__( msg: str, param_name: str, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is invalid. {1}' description = description.format(param_name, msg) @@ -2553,7 +2553,7 @@ def __init__( self, param_name: str, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is required.' description = description.format(param_name) @@ -2605,7 +2605,7 @@ class MediaNotFoundError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type: str, **kwargs: Kwargs) -> None: + def __init__(self, media_type: str, **kwargs: HTTPErrorKeywordArguments) -> None: super().__init__( title='Invalid {0}'.format(media_type), description='Could not parse an empty {0} body'.format(media_type), @@ -2650,7 +2650,7 @@ class MediaMalformedError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type: str, **kwargs: Union[RawHeaders, Kwargs]): + def __init__(self, media_type: str, **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments]): super().__init__( title='Invalid {0}'.format(media_type), description=None, **kwargs ) @@ -2719,7 +2719,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, headers: Optional[RawHeaders] = None, - **kwargs: Kwargs, + **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( title=title, @@ -2751,7 +2751,7 @@ class MultipartParseError(MediaMalformedError): @deprecated_args(allowed_positional=0) def __init__( - self, description: Optional[str] = None, **kwargs: Union[RawHeaders, Kwargs] + self, description: Optional[str] = None, **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments] ) -> None: HTTPBadRequest.__init__( self, From 1d793d45e45c5e126855d5d80e44e5da10789e65 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 16 Oct 2023 21:19:23 +0200 Subject: [PATCH 42/65] Rename Kwargs to HTTPErrorKeywordArgs --- falcon/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon/response.py b/falcon/response.py index edb713a81..9f91fea16 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -706,7 +706,7 @@ def delete_header(self, name): name = name.lower() if name == 'set-cookie': - raise HeaderNotSupported('This method cannot be used to remove ookies') + raise HeaderNotSupported('This method cannot be used to remove cookies') self._headers.pop(name, None) From a3b6aec6f8b4bba8c178c606083c74b4cc3eaf05 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 16 Oct 2023 21:34:33 +0200 Subject: [PATCH 43/65] Import whole package insted of specific types --- falcon/hooks.py | 53 +++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index f994ec01e..dcecc676d 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -19,20 +19,13 @@ from inspect import getmembers from inspect import iscoroutinefunction import re -from typing import Any -from typing import Awaitable -from typing import Callable -from typing import Dict -from typing import List -from typing import Tuple -from typing import TYPE_CHECKING -from typing import Union +import typing as t from falcon.constants import COMBINED_METHODS from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe -if TYPE_CHECKING: +if t.TYPE_CHECKING: import falcon as wsgi from falcon import asgi @@ -40,14 +33,14 @@ r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS)) ) -SynchronousResource = Callable[..., Any] -AsynchronousResource = Callable[..., Awaitable[Any]] -Resource = Union[SynchronousResource, AsynchronousResource] +SynchronousResource = t.Callable[..., t.Any] +AsynchronousResource = t.Callable[..., t.Awaitable[t.Any]] +Resource = t.Union[SynchronousResource, AsynchronousResource] def before( - action: Resource, *args: Any, is_async: bool = False, **kwargs: Any -) -> Callable[[Resource], Resource]: + action: Resource, *args: t.Any, is_async: bool = False, **kwargs: t.Any +) -> t.Callable[[Resource], Resource]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -128,8 +121,8 @@ def let(responder: Resource = responder) -> None: def after( - action: Resource, *args: Any, is_async: bool = False, **kwargs: Any -) -> Callable[[Resource], Resource]: + action: Resource, *args: t.Any, is_async: bool = False, **kwargs: t.Any +) -> t.Callable[[Resource], Resource]: """Execute the given action function *after* the responder. Args: @@ -197,8 +190,8 @@ def let(responder: Resource = responder) -> None: def _wrap_with_after( responder: Resource, action: Resource, - action_args: Any, - action_kwargs: Any, + action_args: t.Any, + action_kwargs: t.Any, is_async: bool, ) -> Resource: """Execute the given action function after a responder method. @@ -232,8 +225,8 @@ async def do_after( self: Resource, req: asgi.Request, resp: asgi.Response, - *args: Any, - **kwargs: Any, + *args: t.Any, + **kwargs: t.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -249,8 +242,8 @@ def do_after( self: Resource, req: wsgi.Request, resp: wsgi.Response, - *args: Any, - **kwargs: Any, + *args: t.Any, + **kwargs: t.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -264,10 +257,10 @@ def do_after( def _wrap_with_before( responder: Resource, action: Resource, - action_args: Tuple[Any, ...], - action_kwargs: Dict[str, Any], + action_args: t.Tuple[t.Any, ...], + action_kwargs: t.Dict[str, t.Any], is_async: bool, -) -> Union[Callable[..., Awaitable[None]], Callable[..., None]]: +) -> t.Union[t.Callable[..., t.Awaitable[None]], t.Callable[..., None]]: """Execute the given action function before a responder method. Args: @@ -299,8 +292,8 @@ async def do_before( self: Resource, req: asgi.Request, resp: asgi.Response, - *args: Any, - **kwargs: Any, + *args: t.Any, + **kwargs: t.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -316,8 +309,8 @@ def do_before( self: Resource, req: wsgi.Request, resp: wsgi.Response, - *args: Any, - **kwargs: Any, + *args: t.Any, + **kwargs: t.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -329,7 +322,7 @@ def do_before( def _merge_responder_args( - args: Tuple[Any, ...], kwargs: Dict[str, Any], argnames: List[str] + args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any], argnames: t.List[str] ) -> None: """Merge responder args into kwargs. From ba4e7480252a7124c5bad274b9f9e8f170a1d1aa Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Tue, 17 Oct 2023 09:20:25 +0200 Subject: [PATCH 44/65] Fix style --- falcon/errors.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/falcon/errors.py b/falcon/errors.py index 41f22238d..13d0aaa70 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -2650,7 +2650,9 @@ class MediaMalformedError(HTTPBadRequest): base articles related to this error (default ``None``). """ - def __init__(self, media_type: str, **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments]): + def __init__( + self, media_type: str, **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments] + ): super().__init__( title='Invalid {0}'.format(media_type), description=None, **kwargs ) @@ -2751,7 +2753,9 @@ class MultipartParseError(MediaMalformedError): @deprecated_args(allowed_positional=0) def __init__( - self, description: Optional[str] = None, **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments] + self, + description: Optional[str] = None, + **kwargs: Union[RawHeaders, HTTPErrorKeywordArguments], ) -> None: HTTPBadRequest.__init__( self, From 077ed148467338457071ae64c2c53a0e5567beb8 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Tue, 17 Oct 2023 19:00:59 +0200 Subject: [PATCH 45/65] Replace Serializer and MediaHandler with protocol --- falcon/app.py | 5 +++-- falcon/media/base.py | 3 +-- falcon/media/handlers.py | 10 +++++++--- falcon/typing.py | 25 ++++++++++++++----------- tests/test_typing.py | 16 ---------------- 5 files changed, 25 insertions(+), 34 deletions(-) delete mode 100644 tests/test_typing.py diff --git a/falcon/app.py b/falcon/app.py index 442d41676..09cf842bd 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -13,7 +13,7 @@ # limitations under the License. """Falcon App class.""" - +import typing from functools import wraps from inspect import iscoroutinefunction import pathlib @@ -36,11 +36,12 @@ from falcon.response import Response from falcon.response import ResponseOptions import falcon.status_codes as status -from falcon.typing import ErrorHandler, ErrorSerializer, SinkPrefix from falcon.util import deprecation from falcon.util import misc from falcon.util.misc import code_to_http_status +if typing.TYPE_CHECKING: + from falcon.typing import ErrorHandler, ErrorSerializer, SinkPrefix # PERF(vytas): On Python 3.5+ (including cythonized modules), # reference via module global is faster than going via self diff --git a/falcon/media/base.py b/falcon/media/base.py index 0cb3d2d6a..ad06b8674 100644 --- a/falcon/media/base.py +++ b/falcon/media/base.py @@ -3,10 +3,9 @@ from typing import IO, Optional, Union from falcon.constants import MEDIA_JSON -from falcon.typing import Serializer -class BaseHandler(Serializer, metaclass=abc.ABCMeta): +class BaseHandler(metaclass=abc.ABCMeta): """Abstract Base Class for an internet media type handler.""" # NOTE(kgriffs): The following special methods are used to enable an diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py index d2b8618b6..0fa23069f 100644 --- a/falcon/media/handlers.py +++ b/falcon/media/handlers.py @@ -1,3 +1,4 @@ +import typing from collections import UserDict import functools @@ -10,11 +11,14 @@ from falcon.media.multipart import MultipartFormHandler from falcon.media.multipart import MultipartParseOptions from falcon.media.urlencoded import URLEncodedFormHandler -from falcon.typing import MediaHandlers from falcon.util import deprecation from falcon.util import misc from falcon.vendor import mimeparse +if typing.TYPE_CHECKING: + from typing import Mapping + from falcon.typing import Serializer + class MissingDependencyHandler: """Placeholder handler that always raises an error. @@ -35,13 +39,13 @@ def _raise(self, *args, **kwargs): serialize = deserialize = _raise -class Handlers(MediaHandlers, UserDict): +class Handlers(UserDict): """A :class:`dict`-like object that manages Internet media type handlers.""" def __init__(self, initial=None): self._resolve = self._create_resolver() - handlers = initial or { + handlers: Mapping[str, Serializer] = initial or { MEDIA_JSON: JSONHandler(), MEDIA_MULTIPART: MultipartFormHandler(), MEDIA_URLENCODED: URLEncodedFormHandler(), diff --git a/falcon/typing.py b/falcon/typing.py index 645fd6a87..540a9070a 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -30,21 +30,24 @@ from falcon.request import Request from falcon.response import Response -Link = Dict[str, str] + from typing import Protocol + class Serializer(Protocol): + def serialize( + self, + media: MutableMapping[str, Union[str, int, None, Link]], + content_type: str, + ) -> bytes: + ... -class Serializer: - def serialize( - self, media: MutableMapping[str, Union[str, int, None, Link]], content_type: str - ) -> bytes: - raise NotImplementedError() + class MediaHandlers(Protocol): + def _resolve( + self, media_type: str, default: str, raise_not_found: bool = False + ) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]: + ... -class MediaHandlers: - def _resolve( - self, media_type: str, default: str, raise_not_found: bool = False - ) -> Tuple[Serializer, Optional[Callable], Optional[Callable]]: - raise NotImplementedError() +Link = Dict[str, str] # Error handlers diff --git a/tests/test_typing.py b/tests/test_typing.py deleted file mode 100644 index 417024f64..000000000 --- a/tests/test_typing.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from falcon.typing import MediaHandlers -from falcon.typing import Serializer - - -class TestSerializer: - def test_interface_raises_not_implemented(self) -> None: - with pytest.raises(NotImplementedError): - Serializer().serialize({'data': 'any'}, 'any') - - -class TestMediaHandlers: - def test_interface_raises_not_implemented(self) -> None: - with pytest.raises(NotImplementedError): - MediaHandlers()._resolve('any', 'any', False) From fbfc4b46aee18700ced08d9edb62e30a90c35059 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Fri, 27 Oct 2023 21:05:34 +0200 Subject: [PATCH 46/65] Add assertion reason message --- falcon/inspect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/falcon/inspect.py b/falcon/inspect.py index fe61d0a71..4aa728f77 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -239,7 +239,10 @@ def _traverse(roots: List[CompiledRouterNode], parent: str) -> None: source_info = _get_source_info(real_func) internal = _is_internal(real_func) - assert source_info + assert source_info, ( + 'This is for type checking only, as here source ' + 'info will always be a string' + ) method_info = RouteMethodInfo( method, source_info, real_func.__name__, internal ) From b5e704f479bf3cdbeeaf02f33bf383731834aae2 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sun, 29 Oct 2023 18:29:55 +0100 Subject: [PATCH 47/65] Fix import issue --- falcon/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/falcon/app.py b/falcon/app.py index 09cf842bd..f3f9a5a6d 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -13,7 +13,6 @@ # limitations under the License. """Falcon App class.""" -import typing from functools import wraps from inspect import iscoroutinefunction import pathlib @@ -36,13 +35,11 @@ from falcon.response import Response from falcon.response import ResponseOptions import falcon.status_codes as status +from falcon.typing import ErrorHandler, ErrorSerializer, SinkPrefix from falcon.util import deprecation from falcon.util import misc from falcon.util.misc import code_to_http_status -if typing.TYPE_CHECKING: - from falcon.typing import ErrorHandler, ErrorSerializer, SinkPrefix - # PERF(vytas): On Python 3.5+ (including cythonized modules), # reference via module global is faster than going via self _BODILESS_STATUS_CODES = frozenset( @@ -654,7 +651,7 @@ def add_static_route( self._static_routes.insert(0, (sr, sr, False)) self._update_sink_and_static_routes() - def add_sink(self, sink: Callable, prefix: SinkPrefix = r'/'): + def add_sink(self, sink: Callable, prefix: SinkPrefix = r'/') -> None: """Register a sink method for the App. If no route matches a request, but the path in the requested URI From 95ede1bcc0c4033a1e7c0016c25ae45bfbddb6ec Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 30 Oct 2023 19:31:49 +0100 Subject: [PATCH 48/65] Fix import order --- falcon/media/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py index 0fa23069f..736e2bb79 100644 --- a/falcon/media/handlers.py +++ b/falcon/media/handlers.py @@ -1,6 +1,6 @@ -import typing from collections import UserDict import functools +import typing from falcon import errors from falcon.constants import MEDIA_JSON From a0a481cb7f0f071cd46d820bba83b98237a6f15e Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sun, 5 Nov 2023 12:36:10 +0100 Subject: [PATCH 49/65] Fix coverage issues --- falcon/hooks.py | 2 +- falcon/media/handlers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index dcecc676d..354f0d0e1 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -25,7 +25,7 @@ from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe -if t.TYPE_CHECKING: +if t.TYPE_CHECKING: # pragma: no cover import falcon as wsgi from falcon import asgi diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py index 736e2bb79..f40f6312b 100644 --- a/falcon/media/handlers.py +++ b/falcon/media/handlers.py @@ -15,7 +15,7 @@ from falcon.util import misc from falcon.vendor import mimeparse -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from typing import Mapping from falcon.typing import Serializer From b2b94d8e5f33771f29ce51b9ef4ff6ef50facb66 Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Sun, 5 Nov 2023 15:10:27 +0100 Subject: [PATCH 50/65] Add ResponderOrResource and Action types --- falcon/hooks.py | 50 ++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 354f0d0e1..45c477d24 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -33,14 +33,15 @@ r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS)) ) -SynchronousResource = t.Callable[..., t.Any] -AsynchronousResource = t.Callable[..., t.Awaitable[t.Any]] -Resource = t.Union[SynchronousResource, AsynchronousResource] +Resource = object +Responder = t.Callable +ResponderOrResource = t.Union[Responder, Resource] +Action = t.Callable def before( - action: Resource, *args: t.Any, is_async: bool = False, **kwargs: t.Any -) -> t.Callable[[Resource], Resource]: + action: Action, *args: t.Any, is_async: bool = False, **kwargs: t.Any +) -> t.Callable[[ResponderOrResource], ResponderOrResource]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -90,7 +91,7 @@ def do_something(req, resp, resource, params): *action*. """ - def _before(responder_or_resource: Resource) -> Resource: + def _before(responder_or_resource: ResponderOrResource) -> ResponderOrResource: if isinstance(responder_or_resource, type): resource = responder_or_resource @@ -100,7 +101,9 @@ def _before(responder_or_resource: Resource) -> Resource: # responder in the do_before_all closure; otherwise, they # will capture the same responder variable that is shared # between iterations of the for loop, above. - def let(responder: Resource = responder) -> None: + responder = t.cast(Responder, responder) + + def let(responder: Responder = responder) -> None: do_before_all = _wrap_with_before( responder, action, args, kwargs, is_async ) @@ -112,7 +115,7 @@ def let(responder: Resource = responder) -> None: return resource else: - responder = responder_or_resource + responder = t.cast(Responder, responder_or_resource) do_before_one = _wrap_with_before(responder, action, args, kwargs, is_async) return do_before_one @@ -121,8 +124,8 @@ def let(responder: Resource = responder) -> None: def after( - action: Resource, *args: t.Any, is_async: bool = False, **kwargs: t.Any -) -> t.Callable[[Resource], Resource]: + action: Action, *args: t.Any, is_async: bool = False, **kwargs: t.Any +) -> t.Callable[[ResponderOrResource], ResponderOrResource]: """Execute the given action function *after* the responder. Args: @@ -155,14 +158,15 @@ def after( *action*. """ - def _after(responder_or_resource: Resource) -> Resource: + def _after(responder_or_resource: ResponderOrResource) -> ResponderOrResource: if isinstance(responder_or_resource, type): - resource = responder_or_resource + resource = t.cast(Resource, responder_or_resource) for responder_name, responder in getmembers(resource, callable): if _DECORABLE_METHOD_NAME.match(responder_name): + responder = t.cast(Responder, responder) - def let(responder: Resource = responder) -> None: + def let(responder: Responder = responder) -> None: do_after_all = _wrap_with_after( responder, action, args, kwargs, is_async ) @@ -174,7 +178,7 @@ def let(responder: Resource = responder) -> None: return resource else: - responder = responder_or_resource + responder = t.cast(Responder, responder_or_resource) do_after_one = _wrap_with_after(responder, action, args, kwargs, is_async) return do_after_one @@ -188,12 +192,12 @@ def let(responder: Resource = responder) -> None: def _wrap_with_after( - responder: Resource, - action: Resource, + responder: Responder, + action: Action, action_args: t.Any, action_kwargs: t.Any, is_async: bool, -) -> Resource: +) -> Responder: """Execute the given action function after a responder method. Args: @@ -222,7 +226,7 @@ def _wrap_with_after( @wraps(responder) async def do_after( - self: Resource, + self: ResponderOrResource, req: asgi.Request, resp: asgi.Response, *args: t.Any, @@ -239,7 +243,7 @@ async def do_after( @wraps(responder) def do_after( - self: Resource, + self: ResponderOrResource, req: wsgi.Request, resp: wsgi.Response, *args: t.Any, @@ -255,8 +259,8 @@ def do_after( def _wrap_with_before( - responder: Resource, - action: Resource, + responder: Responder, + action: Action, action_args: t.Tuple[t.Any, ...], action_kwargs: t.Dict[str, t.Any], is_async: bool, @@ -289,7 +293,7 @@ def _wrap_with_before( @wraps(responder) async def do_before( - self: Resource, + self: ResponderOrResource, req: asgi.Request, resp: asgi.Response, *args: t.Any, @@ -306,7 +310,7 @@ async def do_before( @wraps(responder) def do_before( - self: Resource, + self: ResponderOrResource, req: wsgi.Request, resp: wsgi.Response, *args: t.Any, From 53df7c07c1496f4c5cc404edbfda4d13a09678cc Mon Sep 17 00:00:00 2001 From: Piotr Kopalko Date: Mon, 30 Oct 2023 19:40:56 +0100 Subject: [PATCH 51/65] Improve responders typing --- falcon/hooks.py | 158 ++++++++++++++++++++++++------------- falcon/testing/resource.py | 120 ++++++++++++++++++++-------- falcon/typing.py | 4 +- tests/test_after_hooks.py | 6 +- 4 files changed, 195 insertions(+), 93 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 45c477d24..b2f6c4ce8 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -19,29 +19,64 @@ from inspect import getmembers from inspect import iscoroutinefunction import re -import typing as t +import typing from falcon.constants import COMBINED_METHODS from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe -if t.TYPE_CHECKING: # pragma: no cover +if typing.TYPE_CHECKING: # pragma: no cover import falcon as wsgi from falcon import asgi + ResponderParams = typing.ParamSpec('ResponderParams') + + class SyncResponder(typing.Protocol[ResponderParams]): + def __call__( + self, + responder: SyncResponderOrResource, + req: wsgi.Request, + resp: wsgi.Response, + *args: ResponderParams.args, + **kwargs: ResponderParams.kwargs, + ) -> None: + ... + + class AsyncResponder(typing.Protocol): + async def __call__( + self, + responder: AsyncResponderOrResource, + req: asgi.Request, + resp: asgi.Response, + *args: ResponderParams.args, + **kwargs: ResponderParams.kwargs, + ) -> None: + ... + + Responder = typing.Union[SyncResponder, AsyncResponder] + Resource = object + SyncResponderOrResource = typing.Union[SyncResponder, Resource] + AsyncResponderOrResource = typing.Union[AsyncResponder, Resource] + ResponderOrResource = typing.Union[Responder, Resource] + SynchronousAction = typing.Callable[..., typing.Any] + AsynchronousAction = typing.Callable[..., typing.Awaitable[typing.Any]] + Action = typing.Union[SynchronousAction, AsynchronousAction] +else: + Resource = object + SynchronousAction = typing.Callable[..., typing.Any] + AsynchronousAction = typing.Callable[..., typing.Awaitable[typing.Any]] + SyncResponder = typing.Callable + AsyncResponder = typing.Awaitable + Responder = typing.Union[SyncResponder, AsyncResponder] + _DECORABLE_METHOD_NAME = re.compile( r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS)) ) -Resource = object -Responder = t.Callable -ResponderOrResource = t.Union[Responder, Resource] -Action = t.Callable - def before( - action: Action, *args: t.Any, is_async: bool = False, **kwargs: t.Any -) -> t.Callable[[ResponderOrResource], ResponderOrResource]: + action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any +) -> typing.Callable[[ResponderOrResource], ResponderOrResource]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -93,29 +128,28 @@ def do_something(req, resp, resource, params): def _before(responder_or_resource: ResponderOrResource) -> ResponderOrResource: if isinstance(responder_or_resource, type): - resource = responder_or_resource - - for responder_name, responder in getmembers(resource, callable): + for responder_name, responder in getmembers( + responder_or_resource, callable + ): if _DECORABLE_METHOD_NAME.match(responder_name): # This pattern is necessary to capture the current value of # responder in the do_before_all closure; otherwise, they # will capture the same responder variable that is shared # between iterations of the for loop, above. - responder = t.cast(Responder, responder) - def let(responder: Responder = responder) -> None: + def let(responder: typing.Callable = responder) -> None: do_before_all = _wrap_with_before( responder, action, args, kwargs, is_async ) - setattr(resource, responder_name, do_before_all) + setattr(responder_or_resource, responder_name, do_before_all) let() - return resource + return responder_or_resource else: - responder = t.cast(Responder, responder_or_resource) + responder = typing.cast(Responder, responder_or_resource) do_before_one = _wrap_with_before(responder, action, args, kwargs, is_async) return do_before_one @@ -124,8 +158,8 @@ def let(responder: Responder = responder) -> None: def after( - action: Action, *args: t.Any, is_async: bool = False, **kwargs: t.Any -) -> t.Callable[[ResponderOrResource], ResponderOrResource]: + action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any +) -> typing.Callable[[ResponderOrResource], ResponderOrResource]: """Execute the given action function *after* the responder. Args: @@ -160,25 +194,24 @@ def after( def _after(responder_or_resource: ResponderOrResource) -> ResponderOrResource: if isinstance(responder_or_resource, type): - resource = t.cast(Resource, responder_or_resource) - - for responder_name, responder in getmembers(resource, callable): + for responder_name, responder in getmembers( + responder_or_resource, callable + ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = t.cast(Responder, responder) - def let(responder: Responder = responder) -> None: + def let(responder: Responder | typing.Callable = responder) -> None: do_after_all = _wrap_with_after( responder, action, args, kwargs, is_async ) - setattr(resource, responder_name, do_after_all) + setattr(responder_or_resource, responder_name, do_after_all) let() - return resource + return responder_or_resource else: - responder = t.cast(Responder, responder_or_resource) + responder = typing.cast(Responder, responder_or_resource) do_after_one = _wrap_with_after(responder, action, args, kwargs, is_async) return do_after_one @@ -194,8 +227,8 @@ def let(responder: Responder = responder) -> None: def _wrap_with_after( responder: Responder, action: Action, - action_args: t.Any, - action_kwargs: t.Any, + action_args: typing.Any, + action_kwargs: typing.Any, is_async: bool, ) -> Responder: """Execute the given action function after a responder method. @@ -214,40 +247,44 @@ def _wrap_with_after( responder_argnames = get_argnames(responder) extra_argnames = responder_argnames[2:] # Skip req, resp + do_after_responder: Responder if is_async or iscoroutinefunction(responder): # NOTE(kgriffs): I manually verified that the implicit "else" branch # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = _wrap_non_coroutine_unsafe(action) + async_action = typing.cast( + AsynchronousAction, _wrap_non_coroutine_unsafe(action) + ) else: - async_action = action + async_action = typing.cast(AsynchronousAction, action) + async_responder = typing.cast(AsyncResponder, responder) @wraps(responder) async def do_after( - self: ResponderOrResource, + self: AsyncResponderOrResource, req: asgi.Request, resp: asgi.Response, - *args: t.Any, - **kwargs: t.Any, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) - - await responder(self, req, resp, **kwargs) - assert async_action + await async_responder(self, req, resp, **kwargs) await async_action(req, resp, self, *action_args, **action_kwargs) + do_after_responder = typing.cast(AsyncResponder, do_after) else: + responder = typing.cast(SyncResponder, responder) @wraps(responder) def do_after( - self: ResponderOrResource, + self: SyncResponderOrResource, req: wsgi.Request, resp: wsgi.Response, - *args: t.Any, - **kwargs: t.Any, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -255,16 +292,17 @@ def do_after( responder(self, req, resp, **kwargs) action(req, resp, self, *action_args, **action_kwargs) - return do_after + do_after_responder = typing.cast(SyncResponder, do_after) + return do_after_responder def _wrap_with_before( responder: Responder, action: Action, - action_args: t.Tuple[t.Any, ...], - action_kwargs: t.Dict[str, t.Any], + action_args: typing.Tuple[typing.Any, ...], + action_kwargs: typing.Dict[str, typing.Any], is_async: bool, -) -> t.Union[t.Callable[..., t.Awaitable[None]], t.Callable[..., None]]: +) -> Responder: """Execute the given action function before a responder method. Args: @@ -281,40 +319,45 @@ def _wrap_with_before( responder_argnames = get_argnames(responder) extra_argnames = responder_argnames[2:] # Skip req, resp + do_before_responder: Responder if is_async or iscoroutinefunction(responder): # NOTE(kgriffs): I manually verified that the implicit "else" branch # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = _wrap_non_coroutine_unsafe(action) + async_action = typing.cast( + AsynchronousAction, _wrap_non_coroutine_unsafe(action) + ) else: - async_action = action + async_action = typing.cast(AsynchronousAction, action) + async_responder = typing.cast(AsyncResponder, responder) @wraps(responder) async def do_before( - self: ResponderOrResource, + self: AsyncResponderOrResource, req: asgi.Request, resp: asgi.Response, - *args: t.Any, - **kwargs: t.Any, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) - assert async_action await async_action(req, resp, self, kwargs, *action_args, **action_kwargs) - await responder(self, req, resp, **kwargs) + await async_responder(self, req, resp, **kwargs) + do_before_responder = typing.cast(AsyncResponder, do_before) else: + responder = typing.cast(SyncResponder, responder) @wraps(responder) def do_before( - self: ResponderOrResource, + self: SyncResponderOrResource, req: wsgi.Request, resp: wsgi.Response, - *args: t.Any, - **kwargs: t.Any, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -322,11 +365,14 @@ def do_before( action(req, resp, self, kwargs, *action_args, **action_kwargs) responder(self, req, resp, **kwargs) - return do_before + do_before_responder = typing.cast(SyncResponder, do_before) + return do_before_responder def _merge_responder_args( - args: t.Tuple[t.Any, ...], kwargs: t.Dict[str, t.Any], argnames: t.List[str] + args: typing.Tuple[typing.Any, ...], + kwargs: typing.Dict[str, typing.Any], + argnames: typing.List[str], ) -> None: """Merge responder args into kwargs. diff --git a/falcon/testing/resource.py b/falcon/testing/resource.py index c20854a3e..bb738cf5f 100644 --- a/falcon/testing/resource.py +++ b/falcon/testing/resource.py @@ -22,13 +22,26 @@ resource = testing.SimpleTestResource() """ +from __future__ import annotations from json import dumps as json_dumps +import typing import falcon +if typing.TYPE_CHECKING: # pragma: no cover + from falcon import app as wsgi + from falcon.asgi import app as asgi + from falcon.hooks import ResponderOrResource + from falcon.typing import RawHeaders -def capture_responder_args(req, resp, resource, params): + +def capture_responder_args( + req: wsgi.Request, + resp: wsgi.Response, + resource: ResponderOrResource, + params: typing.Mapping[str, str], +) -> None: """Before hook for capturing responder arguments. Adds the following attributes to the hooked responder's resource @@ -49,41 +62,53 @@ def capture_responder_args(req, resp, resource, params): * `capture-req-media` """ - resource.captured_req = req - resource.captured_resp = resp - resource.captured_kwargs = params + simple_resource = typing.cast(SimpleTestResource, resource) + simple_resource.captured_req = req + simple_resource.captured_resp = resp + simple_resource.captured_kwargs = params - resource.captured_req_media = None - resource.captured_req_body = None + simple_resource.captured_req_media = None + simple_resource.captured_req_body = None num_bytes = req.get_header('capture-req-body-bytes') if num_bytes: - resource.captured_req_body = req.stream.read(int(num_bytes)) + simple_resource.captured_req_body = req.stream.read(int(num_bytes)) elif req.get_header('capture-req-media'): - resource.captured_req_media = req.get_media() + simple_resource.captured_req_media = req.get_media() -async def capture_responder_args_async(req, resp, resource, params): +async def capture_responder_args_async( + req: asgi.Request, + resp: asgi.Response, + resource: ResponderOrResource, + params: typing.Mapping[str, str], +) -> None: """Before hook for capturing responder arguments. An asynchronous version of :meth:`~falcon.testing.capture_responder_args`. """ - resource.captured_req = req - resource.captured_resp = resp - resource.captured_kwargs = params + simple_resource = typing.cast(SimpleTestResource, resource) + simple_resource.captured_req = req + simple_resource.captured_resp = resp + simple_resource.captured_kwargs = params - resource.captured_req_media = None - resource.captured_req_body = None + simple_resource.captured_req_media = None + simple_resource.captured_req_body = None num_bytes = req.get_header('capture-req-body-bytes') if num_bytes: - resource.captured_req_body = await req.stream.read(int(num_bytes)) + simple_resource.captured_req_body = await req.stream.read(int(num_bytes)) elif req.get_header('capture-req-media'): - resource.captured_req_media = await req.get_media() + simple_resource.captured_req_media = await req.get_media() -def set_resp_defaults(req, resp, resource, params): +def set_resp_defaults( + req: wsgi.Request, + resp: wsgi.Response, + resource: ResponderOrResource, + params: typing.Mapping[str, str], +) -> None: """Before hook for setting default response properties. This hook simply sets the the response body, status, @@ -92,18 +117,23 @@ def set_resp_defaults(req, resp, resource, params): that are assumed to be defined on the resource object. """ + simple_resource = typing.cast(SimpleTestResource, resource) + if simple_resource._default_status is not None: + resp.status = simple_resource._default_status - if resource._default_status is not None: - resp.status = resource._default_status - - if resource._default_body is not None: - resp.text = resource._default_body + if simple_resource._default_body is not None: + resp.text = simple_resource._default_body - if resource._default_headers is not None: - resp.set_headers(resource._default_headers) + if simple_resource._default_headers is not None: + resp.set_headers(simple_resource._default_headers) -async def set_resp_defaults_async(req, resp, resource, params): +async def set_resp_defaults_async( + req: asgi.Request, + resp: asgi.Response, + resource: ResponderOrResource, + params: typing.Mapping[str, str], +) -> None: """Wrap :meth:`~falcon.testing.set_resp_defaults` in a coroutine.""" set_resp_defaults(req, resp, resource, params) @@ -145,7 +175,13 @@ class SimpleTestResource: responder methods. """ - def __init__(self, status=None, body=None, json=None, headers=None): + def __init__( + self, + status: typing.Optional[str] = None, + body: typing.Optional[str] = None, + json: typing.Optional[dict[str, str]] = None, + headers: typing.Optional[RawHeaders] = None, + ): self._default_status = status self._default_headers = headers @@ -154,14 +190,22 @@ def __init__(self, status=None, body=None, json=None, headers=None): msg = 'Either json or body may be specified, but not both' raise ValueError(msg) - self._default_body = json_dumps(json, ensure_ascii=False) + self._default_body: typing.Optional[str] = json_dumps( + json, ensure_ascii=False + ) else: self._default_body = body - self.captured_req = None - self.captured_resp = None - self.captured_kwargs = None + self.captured_req: typing.Optional[ + typing.Union[wsgi.Request, asgi.Request] + ] = None + self.captured_resp: typing.Optional[ + typing.Union[wsgi.Response, asgi.Response] + ] = None + self.captured_kwargs: typing.Optional[typing.Any] = None + self.captured_req_media: typing.Optional[typing.Any] = None + self.captured_req_body: typing.Optional[str] = None @property def called(self): @@ -169,12 +213,16 @@ def called(self): @falcon.before(capture_responder_args) @falcon.before(set_resp_defaults) - def on_get(self, req, resp, **kwargs): + def on_get( + self, req: wsgi.Request, resp: wsgi.Response, **kwargs: typing.Any + ) -> None: pass @falcon.before(capture_responder_args) @falcon.before(set_resp_defaults) - def on_post(self, req, resp, **kwargs): + def on_post( + self, req: wsgi.Request, resp: wsgi.Response, **kwargs: typing.Any + ) -> None: pass @@ -218,10 +266,14 @@ class SimpleTestResourceAsync(SimpleTestResource): @falcon.before(capture_responder_args_async) @falcon.before(set_resp_defaults_async) - async def on_get(self, req, resp, **kwargs): + async def on_get( + self, req: asgi.Request, resp: asgi.Response, **kwargs: typing.Any + ) -> None: pass @falcon.before(capture_responder_args_async) @falcon.before(set_resp_defaults_async) - async def on_post(self, req, resp, **kwargs): + async def on_post( + self, req: asgi.Request, resp: asgi.Response, **kwargs: typing.Any + ) -> None: pass diff --git a/falcon/typing.py b/falcon/typing.py index 540a9070a..aff77e039 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -27,11 +27,11 @@ from typing import Union if TYPE_CHECKING: + from typing import Protocol + from falcon.request import Request from falcon.response import Response - from typing import Protocol - class Serializer(Protocol): def serialize( self, diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py index 4f95914b7..06eb2adfd 100644 --- a/tests/test_after_hooks.py +++ b/tests/test_after_hooks.py @@ -1,10 +1,13 @@ import functools import json +import typing import pytest import falcon +from falcon import app as wsgi from falcon import testing +from falcon.hooks import Resource from _util import create_app, create_resp # NOQA @@ -346,8 +349,9 @@ class ResourceAwareGameHook: VALUES = ('rock', 'scissors', 'paper') @classmethod - def __call__(cls, req, resp, resource): + def __call__(cls, req: wsgi.Request, resp: wsgi.Response, resource: Resource): assert resource + resource = typing.cast(HandGame, resource) assert resource.seed in cls.VALUES assert resp.text == 'Responder called.' From 7b2df8b135e65dd0e12d4ae10ba38fc9f09a04c5 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 16:15:28 +0200 Subject: [PATCH 52/65] style: run ruff --- falcon/app.py | 1 + falcon/app_helpers.py | 1 - falcon/asgi_spec.py | 1 - falcon/errors.py | 1 - falcon/hooks.py | 9 +++------ falcon/http_error.py | 1 - falcon/http_status.py | 4 ---- falcon/inspect.py | 1 + falcon/media/handlers.py | 1 - falcon/redirects.py | 4 ---- falcon/request.py | 3 +-- falcon/response.py | 1 - falcon/testing/resource.py | 9 +++++---- falcon/typing.py | 1 - 14 files changed, 11 insertions(+), 27 deletions(-) diff --git a/falcon/app.py b/falcon/app.py index a170cde77..53cffe397 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -13,6 +13,7 @@ # limitations under the License. """Falcon App class.""" + from functools import wraps from inspect import iscoroutinefunction import pathlib diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index d5677cc04..db6d7cc24 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -13,7 +13,6 @@ # limitations under the License. """Utilities for the App class.""" -from __future__ import annotations from __future__ import annotations diff --git a/falcon/asgi_spec.py b/falcon/asgi_spec.py index c695887ca..bca282b37 100644 --- a/falcon/asgi_spec.py +++ b/falcon/asgi_spec.py @@ -13,7 +13,6 @@ # limitations under the License. """Constants, etc. defined by the ASGI specification.""" -from __future__ import annotations from __future__ import annotations diff --git a/falcon/errors.py b/falcon/errors.py index ef2da3673..afc1faa8a 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -33,7 +33,6 @@ def on_get(self, req, resp): # -- snip -- """ -from __future__ import annotations from __future__ import annotations diff --git a/falcon/hooks.py b/falcon/hooks.py index 04239d439..2d6879ae2 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -13,7 +13,6 @@ # limitations under the License. """Hook decorators.""" -from __future__ import annotations from __future__ import annotations @@ -41,8 +40,7 @@ def __call__( resp: wsgi.Response, *args: ResponderParams.args, **kwargs: ResponderParams.kwargs, - ) -> None: - ... + ) -> None: ... class AsyncResponder(typing.Protocol): async def __call__( @@ -52,8 +50,7 @@ async def __call__( resp: asgi.Response, *args: ResponderParams.args, **kwargs: ResponderParams.kwargs, - ) -> None: - ... + ) -> None: ... Responder = typing.Union[SyncResponder, AsyncResponder] Resource = object @@ -203,7 +200,7 @@ def _after(responder_or_resource: ResponderOrResource) -> ResponderOrResource: responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = t.cast(Responder, responder) + responder = typing.cast(Responder, responder) def let(responder: Responder | typing.Callable = responder) -> None: do_after_all = _wrap_with_after( diff --git a/falcon/http_error.py b/falcon/http_error.py index 7f631b353..3f4af635c 100644 --- a/falcon/http_error.py +++ b/falcon/http_error.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """HTTPError exception class.""" -from __future__ import annotations from __future__ import annotations diff --git a/falcon/http_status.py b/falcon/http_status.py index f19099b0d..df7e0d455 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """HTTPStatus exception class.""" -from __future__ import annotations - -from typing import Optional -from typing import TYPE_CHECKING from __future__ import annotations diff --git a/falcon/inspect.py b/falcon/inspect.py index e4c7d23c7..9aac44cb0 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -13,6 +13,7 @@ # limitations under the License. """Inspect utilities for falcon applications.""" + from __future__ import annotations from functools import partial diff --git a/falcon/media/handlers.py b/falcon/media/handlers.py index d168fa2cc..68dbbe88b 100644 --- a/falcon/media/handlers.py +++ b/falcon/media/handlers.py @@ -18,7 +18,6 @@ from falcon.vendor import mimeparse - class MissingDependencyHandler(BinaryBaseHandlerWS): """Placeholder handler that always raises an error. diff --git a/falcon/redirects.py b/falcon/redirects.py index 26bb885f8..7d2381d47 100644 --- a/falcon/redirects.py +++ b/falcon/redirects.py @@ -12,10 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """HTTPStatus specializations for 3xx redirects.""" -from __future__ import annotations - -from typing import Optional -from typing import TYPE_CHECKING from __future__ import annotations diff --git a/falcon/request.py b/falcon/request.py index a92cb88b2..f8fc6f4ab 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -11,7 +11,6 @@ # limitations under the License. """Request class.""" -from __future__ import annotations from __future__ import annotations @@ -2093,7 +2092,7 @@ class RequestOptions: auto_parse_qs_csv: bool strip_url_path_trailing_slash: bool default_media_type: str - media_handlers: UserDict + media_handlers: Handlers __slots__ = ( 'keep_blank_qs_values', diff --git a/falcon/response.py b/falcon/response.py index b8c7d72cb..e96f2ba2f 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -13,7 +13,6 @@ # limitations under the License. """Response class.""" -from __future__ import annotations from __future__ import annotations diff --git a/falcon/testing/resource.py b/falcon/testing/resource.py index bb738cf5f..022adb22c 100644 --- a/falcon/testing/resource.py +++ b/falcon/testing/resource.py @@ -22,6 +22,7 @@ resource = testing.SimpleTestResource() """ + from __future__ import annotations from json import dumps as json_dumps @@ -29,7 +30,7 @@ import falcon -if typing.TYPE_CHECKING: # pragma: no cover +if typing.TYPE_CHECKING: # pragma: no cover from falcon import app as wsgi from falcon.asgi import app as asgi from falcon.hooks import ResponderOrResource @@ -197,9 +198,9 @@ def __init__( else: self._default_body = body - self.captured_req: typing.Optional[ - typing.Union[wsgi.Request, asgi.Request] - ] = None + self.captured_req: typing.Optional[typing.Union[wsgi.Request, asgi.Request]] = ( + None + ) self.captured_resp: typing.Optional[ typing.Union[wsgi.Response, asgi.Response] ] = None diff --git a/falcon/typing.py b/falcon/typing.py index 9f64cb40a..a7095bb56 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """Shorthand definitions for more complex types.""" -from __future__ import annotations from __future__ import annotations From 70eef75920ed1ac77e4343860f6fd5930d64b0df Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 16:59:33 +0200 Subject: [PATCH 53/65] typing: improve hooks --- falcon/hooks.py | 102 ++++++++++++++----------------------- falcon/testing/resource.py | 18 +++---- falcon/typing.py | 2 + 3 files changed, 50 insertions(+), 72 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 2d6879ae2..f99c687e0 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -29,44 +29,33 @@ if typing.TYPE_CHECKING: # pragma: no cover import falcon as wsgi from falcon import asgi + from falcon.typing import Resource - ResponderParams = typing.ParamSpec('ResponderParams') - - class SyncResponder(typing.Protocol[ResponderParams]): + class SyncResponderMethod(typing.Protocol): def __call__( self, - responder: SyncResponderOrResource, + resource: Resource, req: wsgi.Request, resp: wsgi.Response, - *args: ResponderParams.args, - **kwargs: ResponderParams.kwargs, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: ... - class AsyncResponder(typing.Protocol): + class AsyncResponderMethod(typing.Protocol): async def __call__( self, - responder: AsyncResponderOrResource, + resource: Resource, req: asgi.Request, resp: asgi.Response, - *args: ResponderParams.args, - **kwargs: ResponderParams.kwargs, + *args: typing.Any, + **kwargs: typing.Any, ) -> None: ... - Responder = typing.Union[SyncResponder, AsyncResponder] - Resource = object - SyncResponderOrResource = typing.Union[SyncResponder, Resource] - AsyncResponderOrResource = typing.Union[AsyncResponder, Resource] - ResponderOrResource = typing.Union[Responder, Resource] + Responder = typing.Union[SyncResponderMethod, AsyncResponderMethod] SynchronousAction = typing.Callable[..., typing.Any] AsynchronousAction = typing.Callable[..., typing.Awaitable[typing.Any]] Action = typing.Union[SynchronousAction, AsynchronousAction] -else: - Resource = object - SynchronousAction = typing.Callable[..., typing.Any] - AsynchronousAction = typing.Callable[..., typing.Awaitable[typing.Any]] - SyncResponder = typing.Callable - AsyncResponder = typing.Awaitable - Responder = typing.Union[SyncResponder, AsyncResponder] + _R = typing.TypeVar('_R', bound=typing.Union[Responder, Resource]) _DECORABLE_METHOD_NAME = re.compile( @@ -76,7 +65,7 @@ async def __call__( def before( action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any -) -> typing.Callable[[ResponderOrResource], ResponderOrResource]: +) -> typing.Callable[[_R], _R]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -126,42 +115,33 @@ def do_something(req, resp, resource, params): *action*. """ - def _before(responder_or_resource: ResponderOrResource) -> ResponderOrResource: + def _before(responder_or_resource: _R) -> _R: if isinstance(responder_or_resource, type): for responder_name, responder in getmembers( responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - # This pattern is necessary to capture the current value of - # responder in the do_before_all closure; otherwise, they - # will capture the same responder variable that is shared - # between iterations of the for loop, above. - responder = typing.cast(Responder, responder) + do_before_all = _wrap_with_before( + responder, action, args, kwargs, is_async + ) - def let(responder: Responder = responder) -> None: - do_before_all = _wrap_with_before( - responder, action, args, kwargs, is_async - ) - - setattr(responder_or_resource, responder_name, do_before_all) + setattr(responder_or_resource, responder_name, do_before_all) - let() - - return responder_or_resource + return typing.cast(_R, responder_or_resource) else: responder = typing.cast(Responder, responder_or_resource) do_before_one = _wrap_with_before(responder, action, args, kwargs, is_async) - return do_before_one + return typing.cast(_R, do_before_one) return _before def after( action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any -) -> typing.Callable[[ResponderOrResource], ResponderOrResource]: +) -> typing.Callable[[_R], _R]: """Execute the given action function *after* the responder. Args: @@ -194,30 +174,26 @@ def after( *action*. """ - def _after(responder_or_resource: ResponderOrResource) -> ResponderOrResource: + def _after(responder_or_resource: _R) -> _R: if isinstance(responder_or_resource, type): for responder_name, responder in getmembers( responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): responder = typing.cast(Responder, responder) + do_after_all = _wrap_with_after( + responder, action, args, kwargs, is_async + ) - def let(responder: Responder | typing.Callable = responder) -> None: - do_after_all = _wrap_with_after( - responder, action, args, kwargs, is_async - ) - - setattr(responder_or_resource, responder_name, do_after_all) - - let() + setattr(responder_or_resource, responder_name, do_after_all) - return responder_or_resource + return typing.cast(_R, responder_or_resource) else: responder = typing.cast(Responder, responder_or_resource) do_after_one = _wrap_with_after(responder, action, args, kwargs, is_async) - return do_after_one + return typing.cast(_R, do_after_one) return _after @@ -262,11 +238,11 @@ def _wrap_with_after( ) else: async_action = typing.cast(AsynchronousAction, action) - async_responder = typing.cast(AsyncResponder, responder) + async_responder = typing.cast(AsyncResponderMethod, responder) @wraps(responder) async def do_after( - self: AsyncResponderOrResource, + self: Resource, req: asgi.Request, resp: asgi.Response, *args: typing.Any, @@ -277,13 +253,13 @@ async def do_after( await async_responder(self, req, resp, **kwargs) await async_action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = typing.cast(AsyncResponder, do_after) + do_after_responder = typing.cast(AsyncResponderMethod, do_after) else: - responder = typing.cast(SyncResponder, responder) + responder = typing.cast(SyncResponderMethod, responder) @wraps(responder) def do_after( - self: SyncResponderOrResource, + self: Resource, req: wsgi.Request, resp: wsgi.Response, *args: typing.Any, @@ -295,7 +271,7 @@ def do_after( responder(self, req, resp, **kwargs) action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = typing.cast(SyncResponder, do_after) + do_after_responder = typing.cast(SyncResponderMethod, do_after) return do_after_responder @@ -334,11 +310,11 @@ def _wrap_with_before( ) else: async_action = typing.cast(AsynchronousAction, action) - async_responder = typing.cast(AsyncResponder, responder) + async_responder = typing.cast(AsyncResponderMethod, responder) @wraps(responder) async def do_before( - self: AsyncResponderOrResource, + self: Resource, req: asgi.Request, resp: asgi.Response, *args: typing.Any, @@ -350,13 +326,13 @@ async def do_before( await async_action(req, resp, self, kwargs, *action_args, **action_kwargs) await async_responder(self, req, resp, **kwargs) - do_before_responder = typing.cast(AsyncResponder, do_before) + do_before_responder = typing.cast(AsyncResponderMethod, do_before) else: - responder = typing.cast(SyncResponder, responder) + responder = typing.cast(SyncResponderMethod, responder) @wraps(responder) def do_before( - self: SyncResponderOrResource, + self: Resource, req: wsgi.Request, resp: wsgi.Response, *args: typing.Any, @@ -368,7 +344,7 @@ def do_before( action(req, resp, self, kwargs, *action_args, **action_kwargs) responder(self, req, resp, **kwargs) - do_before_responder = typing.cast(SyncResponder, do_before) + do_before_responder = typing.cast(SyncResponderMethod, do_before) return do_before_responder diff --git a/falcon/testing/resource.py b/falcon/testing/resource.py index 022adb22c..68a6ed7cb 100644 --- a/falcon/testing/resource.py +++ b/falcon/testing/resource.py @@ -33,14 +33,14 @@ if typing.TYPE_CHECKING: # pragma: no cover from falcon import app as wsgi from falcon.asgi import app as asgi - from falcon.hooks import ResponderOrResource - from falcon.typing import RawHeaders + from falcon.typing import HeaderList + from falcon.typing import Resource def capture_responder_args( req: wsgi.Request, resp: wsgi.Response, - resource: ResponderOrResource, + resource: Resource, params: typing.Mapping[str, str], ) -> None: """Before hook for capturing responder arguments. @@ -81,7 +81,7 @@ def capture_responder_args( async def capture_responder_args_async( req: asgi.Request, resp: asgi.Response, - resource: ResponderOrResource, + resource: Resource, params: typing.Mapping[str, str], ) -> None: """Before hook for capturing responder arguments. @@ -107,7 +107,7 @@ async def capture_responder_args_async( def set_resp_defaults( req: wsgi.Request, resp: wsgi.Response, - resource: ResponderOrResource, + resource: Resource, params: typing.Mapping[str, str], ) -> None: """Before hook for setting default response properties. @@ -132,7 +132,7 @@ def set_resp_defaults( async def set_resp_defaults_async( req: asgi.Request, resp: asgi.Response, - resource: ResponderOrResource, + resource: Resource, params: typing.Mapping[str, str], ) -> None: """Wrap :meth:`~falcon.testing.set_resp_defaults` in a coroutine.""" @@ -181,7 +181,7 @@ def __init__( status: typing.Optional[str] = None, body: typing.Optional[str] = None, json: typing.Optional[dict[str, str]] = None, - headers: typing.Optional[RawHeaders] = None, + headers: typing.Optional[HeaderList] = None, ): self._default_status = status self._default_headers = headers @@ -267,14 +267,14 @@ class SimpleTestResourceAsync(SimpleTestResource): @falcon.before(capture_responder_args_async) @falcon.before(set_resp_defaults_async) - async def on_get( + async def on_get( # type: ignore[override] self, req: asgi.Request, resp: asgi.Response, **kwargs: typing.Any ) -> None: pass @falcon.before(capture_responder_args_async) @falcon.before(set_resp_defaults_async) - async def on_post( + async def on_post( # type: ignore[override] self, req: asgi.Request, resp: asgi.Response, **kwargs: typing.Any ) -> None: pass diff --git a/falcon/typing.py b/falcon/typing.py index a7095bb56..ffbc3770a 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -51,3 +51,5 @@ Headers = Dict[str, str] HeaderList = Union[Headers, List[Tuple[str, str]]] ResponseStatus = Union[http.HTTPStatus, str, int] + +Resource = object From 0813f6243bc3a5cb5986807daacb0797c35b2281 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 17:36:12 +0200 Subject: [PATCH 54/65] typing: more improvement to hooks, install typing-extensions on <3.8 --- falcon/_typing_extensions.py | 6 ++ falcon/hooks.py | 199 +++++++++++++++++++++-------------- falcon/testing/resource.py | 2 +- falcon/typing.py | 29 +++++ setup.cfg | 1 + 5 files changed, 157 insertions(+), 80 deletions(-) create mode 100644 falcon/_typing_extensions.py diff --git a/falcon/_typing_extensions.py b/falcon/_typing_extensions.py new file mode 100644 index 000000000..711d7ce2b --- /dev/null +++ b/falcon/_typing_extensions.py @@ -0,0 +1,6 @@ +import sys + +if sys.version_info < (3, 8): + from typing_extensions import Protocol as Protocol +else: + from typing import Protocol as Protocol diff --git a/falcon/hooks.py b/falcon/hooks.py index f99c687e0..cedb42f7a 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -20,42 +20,84 @@ from inspect import getmembers from inspect import iscoroutinefunction import re -import typing +from typing import ( + Any, + Awaitable, + Callable, + cast, + Dict, + List, + Tuple, + TYPE_CHECKING, + TypeVar, + Union, +) +from falcon._typing_extensions import Protocol from falcon.constants import COMBINED_METHODS from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe -if typing.TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover import falcon as wsgi from falcon import asgi + from falcon.typing import AsyncResponderMethod from falcon.typing import Resource + from falcon.typing import Responder + from falcon.typing import SyncResponderMethod - class SyncResponderMethod(typing.Protocol): - def __call__( - self, - resource: Resource, - req: wsgi.Request, - resp: wsgi.Response, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: ... - - class AsyncResponderMethod(typing.Protocol): - async def __call__( - self, - resource: Resource, - req: asgi.Request, - resp: asgi.Response, - *args: typing.Any, - **kwargs: typing.Any, - ) -> None: ... - Responder = typing.Union[SyncResponderMethod, AsyncResponderMethod] - SynchronousAction = typing.Callable[..., typing.Any] - AsynchronousAction = typing.Callable[..., typing.Awaitable[typing.Any]] - Action = typing.Union[SynchronousAction, AsynchronousAction] - _R = typing.TypeVar('_R', bound=typing.Union[Responder, Resource]) +class SyncBeforeFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> None: ... + + +class AsyncBeforeFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... + + +BeforeFn = Union[SyncBeforeFn, AsyncBeforeFn] + + +class SyncAfterFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> None: ... + + +class AsyncAfterFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... + + +AfterFn = Union[SyncAfterFn, AsyncAfterFn] +_R = TypeVar('_R', bound=Union[Responder, Resource]) _DECORABLE_METHOD_NAME = re.compile( @@ -64,8 +106,8 @@ async def __call__( def before( - action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any -) -> typing.Callable[[_R], _R]: + action: BeforeFn, *args: Any, is_async: bool = False, **kwargs: Any +) -> Callable[[_R], _R]: """Execute the given action function *before* the responder. The `params` argument that is passed to the hook @@ -121,27 +163,27 @@ def _before(responder_or_resource: _R) -> _R: responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = typing.cast(Responder, responder) + responder = cast(Responder, responder) do_before_all = _wrap_with_before( responder, action, args, kwargs, is_async ) setattr(responder_or_resource, responder_name, do_before_all) - return typing.cast(_R, responder_or_resource) + return cast(_R, responder_or_resource) else: - responder = typing.cast(Responder, responder_or_resource) + responder = cast(Responder, responder_or_resource) do_before_one = _wrap_with_before(responder, action, args, kwargs, is_async) - return typing.cast(_R, do_before_one) + return cast(_R, do_before_one) return _before def after( - action: Action, *args: typing.Any, is_async: bool = False, **kwargs: typing.Any -) -> typing.Callable[[_R], _R]: + action: AfterFn, *args: Any, is_async: bool = False, **kwargs: Any +) -> Callable[[_R], _R]: """Execute the given action function *after* the responder. Args: @@ -180,20 +222,20 @@ def _after(responder_or_resource: _R) -> _R: responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = typing.cast(Responder, responder) + responder = cast(Responder, responder) do_after_all = _wrap_with_after( responder, action, args, kwargs, is_async ) setattr(responder_or_resource, responder_name, do_after_all) - return typing.cast(_R, responder_or_resource) + return cast(_R, responder_or_resource) else: - responder = typing.cast(Responder, responder_or_resource) + responder = cast(Responder, responder_or_resource) do_after_one = _wrap_with_after(responder, action, args, kwargs, is_async) - return typing.cast(_R, do_after_one) + return cast(_R, do_after_one) return _after @@ -205,9 +247,9 @@ def _after(responder_or_resource: _R) -> _R: def _wrap_with_after( responder: Responder, - action: Action, - action_args: typing.Any, - action_kwargs: typing.Any, + action: AfterFn, + action_args: Any, + action_kwargs: Any, is_async: bool, ) -> Responder: """Execute the given action function after a responder method. @@ -233,53 +275,53 @@ def _wrap_with_after( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = typing.cast( - AsynchronousAction, _wrap_non_coroutine_unsafe(action) - ) + async_action = cast(AsyncAfterFn, _wrap_non_coroutine_unsafe(action)) else: - async_action = typing.cast(AsynchronousAction, action) - async_responder = typing.cast(AsyncResponderMethod, responder) + async_action = cast(AsyncAfterFn, action) + async_responder = cast(AsyncResponderMethod, responder) - @wraps(responder) + @wraps(async_responder) async def do_after( self: Resource, req: asgi.Request, resp: asgi.Response, - *args: typing.Any, - **kwargs: typing.Any, + *args: Any, + **kwargs: Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) + await async_responder(self, req, resp, **kwargs) await async_action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = typing.cast(AsyncResponderMethod, do_after) + do_after_responder = cast(AsyncResponderMethod, do_after) else: - responder = typing.cast(SyncResponderMethod, responder) + sync_action = cast(SyncAfterFn, action) + sync_responder = cast(SyncResponderMethod, responder) - @wraps(responder) + @wraps(sync_responder) def do_after( self: Resource, req: wsgi.Request, resp: wsgi.Response, - *args: typing.Any, - **kwargs: typing.Any, + *args: Any, + **kwargs: Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) - responder(self, req, resp, **kwargs) - action(req, resp, self, *action_args, **action_kwargs) + sync_responder(self, req, resp, **kwargs) + sync_action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = typing.cast(SyncResponderMethod, do_after) + do_after_responder = cast(SyncResponderMethod, do_after) return do_after_responder def _wrap_with_before( responder: Responder, - action: Action, - action_args: typing.Tuple[typing.Any, ...], - action_kwargs: typing.Dict[str, typing.Any], + action: BeforeFn, + action_args: Tuple[Any, ...], + action_kwargs: Dict[str, Any], is_async: bool, ) -> Responder: """Execute the given action function before a responder method. @@ -305,20 +347,18 @@ def _wrap_with_before( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = typing.cast( - AsynchronousAction, _wrap_non_coroutine_unsafe(action) - ) + async_action = cast(AsyncBeforeFn, _wrap_non_coroutine_unsafe(action)) else: - async_action = typing.cast(AsynchronousAction, action) - async_responder = typing.cast(AsyncResponderMethod, responder) + async_action = cast(AsyncBeforeFn, action) + async_responder = cast(AsyncResponderMethod, responder) - @wraps(responder) + @wraps(async_responder) async def do_before( self: Resource, req: asgi.Request, resp: asgi.Response, - *args: typing.Any, - **kwargs: typing.Any, + *args: Any, + **kwargs: Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) @@ -326,32 +366,33 @@ async def do_before( await async_action(req, resp, self, kwargs, *action_args, **action_kwargs) await async_responder(self, req, resp, **kwargs) - do_before_responder = typing.cast(AsyncResponderMethod, do_before) + do_before_responder = cast(AsyncResponderMethod, do_before) else: - responder = typing.cast(SyncResponderMethod, responder) + sync_action = cast(SyncBeforeFn, action) + sync_responder = cast(SyncResponderMethod, responder) - @wraps(responder) + @wraps(sync_responder) def do_before( self: Resource, req: wsgi.Request, resp: wsgi.Response, - *args: typing.Any, - **kwargs: typing.Any, + *args: Any, + **kwargs: Any, ) -> None: if args: _merge_responder_args(args, kwargs, extra_argnames) - action(req, resp, self, kwargs, *action_args, **action_kwargs) - responder(self, req, resp, **kwargs) + sync_action(req, resp, self, kwargs, *action_args, **action_kwargs) + sync_responder(self, req, resp, **kwargs) - do_before_responder = typing.cast(SyncResponderMethod, do_before) + do_before_responder = cast(SyncResponderMethod, do_before) return do_before_responder def _merge_responder_args( - args: typing.Tuple[typing.Any, ...], - kwargs: typing.Dict[str, typing.Any], - argnames: typing.List[str], + args: Tuple[Any, ...], + kwargs: Dict[str, Any], + argnames: List[str], ) -> None: """Merge responder args into kwargs. diff --git a/falcon/testing/resource.py b/falcon/testing/resource.py index 68a6ed7cb..14e0854c3 100644 --- a/falcon/testing/resource.py +++ b/falcon/testing/resource.py @@ -40,7 +40,7 @@ def capture_responder_args( req: wsgi.Request, resp: wsgi.Response, - resource: Resource, + resource: object, params: typing.Mapping[str, str], ) -> None: """Before hook for capturing responder arguments. diff --git a/falcon/typing.py b/falcon/typing.py index ffbc3770a..e72c6fcb6 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -27,7 +27,11 @@ Union, ) +from falcon._typing_extensions import Protocol + if TYPE_CHECKING: + import falcon as wsgi + from falcon import asgi from falcon.request import Request from falcon.response import Response @@ -53,3 +57,28 @@ ResponseStatus = Union[http.HTTPStatus, str, int] Resource = object + + +class SyncResponderMethod(Protocol): + def __call__( + self, + resource: Resource, + req: wsgi.Request, + resp: wsgi.Response, + *args: Any, + **kwargs: Any, + ) -> None: ... + + +class AsyncResponderMethod(Protocol): + async def __call__( + self, + resource: Resource, + req: asgi.Request, + resp: asgi.Response, + *args: Any, + **kwargs: Any, + ) -> None: ... + + +Responder = Union[SyncResponderMethod, AsyncResponderMethod] diff --git a/setup.cfg b/setup.cfg index 41c3b93ab..aa079972c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,7 @@ include_package_data = True packages = find: python_requires = >=3.7 install_requires = + typing-extensions;python_version<"3.8" tests_require = testtools requests From 21a9d9cfbd72954222fc7e4de8c38270c5af9022 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 17:58:46 +0200 Subject: [PATCH 55/65] style: run formatters --- falcon/hooks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index cedb42f7a..d12f82e71 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -390,9 +390,7 @@ def do_before( def _merge_responder_args( - args: Tuple[Any, ...], - kwargs: Dict[str, Any], - argnames: List[str], + args: Tuple[Any, ...], kwargs: Dict[str, Any], argnames: List[str] ) -> None: """Merge responder args into kwargs. From 95d7e9e9a2aa4206826498066a407538a1fe45d2 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:14:12 +0200 Subject: [PATCH 56/65] fix: correct typo and add todo note regarding improvements --- falcon/hooks.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index d12f82e71..7b34fb6a1 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -47,6 +47,10 @@ from falcon.typing import SyncResponderMethod +# TODO: if is_async is removed there protocol would no longer be needed, since +# ParamSpec could be used together with Concatenate to use a simple Callable +# to type the before and after function. This approach was prototyped in +# https://github.com/falconry/falcon/pull/2234 class SyncBeforeFn(Protocol): def __call__( self, @@ -97,7 +101,7 @@ def __call__( AfterFn = Union[SyncAfterFn, AsyncAfterFn] -_R = TypeVar('_R', bound=Union[Responder, Resource]) +_R = TypeVar('_R', bound=Union['Responder', 'Resource']) _DECORABLE_METHOD_NAME = re.compile( From c50f4256cea86ec789c29944e0494d8f0a0070b7 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:17:07 +0200 Subject: [PATCH 57/65] docs: improve docs --- falcon/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 7b34fb6a1..7d7ffa39e 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -47,9 +47,9 @@ from falcon.typing import SyncResponderMethod -# TODO: if is_async is removed there protocol would no longer be needed, since +# TODO: if is_async is removed these protocol would no longer be needed, since # ParamSpec could be used together with Concatenate to use a simple Callable -# to type the before and after function. This approach was prototyped in +# to type the before and after functions. This approach was prototyped in # https://github.com/falconry/falcon/pull/2234 class SyncBeforeFn(Protocol): def __call__( From e3141c4590fc4edf3d2a7d50cf0810d3809cc998 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:20:08 +0200 Subject: [PATCH 58/65] fix: use string to refer to type_checking symbols --- falcon/hooks.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 7d7ffa39e..cad1e4ffa 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -167,7 +167,7 @@ def _before(responder_or_resource: _R) -> _R: responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = cast(Responder, responder) + responder = cast('Responder', responder) do_before_all = _wrap_with_before( responder, action, args, kwargs, is_async ) @@ -177,7 +177,7 @@ def _before(responder_or_resource: _R) -> _R: return cast(_R, responder_or_resource) else: - responder = cast(Responder, responder_or_resource) + responder = cast('Responder', responder_or_resource) do_before_one = _wrap_with_before(responder, action, args, kwargs, is_async) return cast(_R, do_before_one) @@ -226,7 +226,7 @@ def _after(responder_or_resource: _R) -> _R: responder_or_resource, callable ): if _DECORABLE_METHOD_NAME.match(responder_name): - responder = cast(Responder, responder) + responder = cast('Responder', responder) do_after_all = _wrap_with_after( responder, action, args, kwargs, is_async ) @@ -236,7 +236,7 @@ def _after(responder_or_resource: _R) -> _R: return cast(_R, responder_or_resource) else: - responder = cast(Responder, responder_or_resource) + responder = cast('Responder', responder_or_resource) do_after_one = _wrap_with_after(responder, action, args, kwargs, is_async) return cast(_R, do_after_one) @@ -282,7 +282,7 @@ def _wrap_with_after( async_action = cast(AsyncAfterFn, _wrap_non_coroutine_unsafe(action)) else: async_action = cast(AsyncAfterFn, action) - async_responder = cast(AsyncResponderMethod, responder) + async_responder = cast('AsyncResponderMethod', responder) @wraps(async_responder) async def do_after( @@ -298,10 +298,10 @@ async def do_after( await async_responder(self, req, resp, **kwargs) await async_action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = cast(AsyncResponderMethod, do_after) + do_after_responder = cast('AsyncResponderMethod', do_after) else: sync_action = cast(SyncAfterFn, action) - sync_responder = cast(SyncResponderMethod, responder) + sync_responder = cast('SyncResponderMethod', responder) @wraps(sync_responder) def do_after( @@ -317,7 +317,7 @@ def do_after( sync_responder(self, req, resp, **kwargs) sync_action(req, resp, self, *action_args, **action_kwargs) - do_after_responder = cast(SyncResponderMethod, do_after) + do_after_responder = cast('SyncResponderMethod', do_after) return do_after_responder @@ -354,7 +354,7 @@ def _wrap_with_before( async_action = cast(AsyncBeforeFn, _wrap_non_coroutine_unsafe(action)) else: async_action = cast(AsyncBeforeFn, action) - async_responder = cast(AsyncResponderMethod, responder) + async_responder = cast('AsyncResponderMethod', responder) @wraps(async_responder) async def do_before( @@ -370,10 +370,10 @@ async def do_before( await async_action(req, resp, self, kwargs, *action_args, **action_kwargs) await async_responder(self, req, resp, **kwargs) - do_before_responder = cast(AsyncResponderMethod, do_before) + do_before_responder = cast('AsyncResponderMethod', do_before) else: sync_action = cast(SyncBeforeFn, action) - sync_responder = cast(SyncResponderMethod, responder) + sync_responder = cast('SyncResponderMethod', responder) @wraps(sync_responder) def do_before( @@ -389,7 +389,7 @@ def do_before( sync_action(req, resp, self, kwargs, *action_args, **action_kwargs) sync_responder(self, req, resp, **kwargs) - do_before_responder = cast(SyncResponderMethod, do_before) + do_before_responder = cast('SyncResponderMethod', do_before) return do_before_responder From b3c5c28bf6c3e863a3134c0e29ac6ce600325f97 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:25:58 +0200 Subject: [PATCH 59/65] test: fix import --- tests/test_after_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py index bbb0f0a58..6448b99a3 100644 --- a/tests/test_after_hooks.py +++ b/tests/test_after_hooks.py @@ -9,7 +9,7 @@ import falcon from falcon import app as wsgi from falcon import testing -from falcon.hooks import Resource +from falcon.typing import Resource # -------------------------------------------------------------------- # Fixtures From 7341f96eab52f98b16080f2f458fa5fd3799f4f5 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:33:45 +0200 Subject: [PATCH 60/65] chore: make python 3.7 happy --- pyproject.toml | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 559256ad8..d086c1811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ "setuptools>=47", "wheel>=0.34", "cython>=0.29.21; python_implementation == 'CPython'", # Skip cython when using pypy + "typing-extensions; python_version<'3.8'", ] [tool.mypy] diff --git a/setup.cfg b/setup.cfg index aa079972c..163d28d95 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ include_package_data = True packages = find: python_requires = >=3.7 install_requires = - typing-extensions;python_version<"3.8" + typing-extensions; python_version<"3.8" tests_require = testtools requests From 3296944c9f26d72468ff6b70c46ebd681f956140 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Mon, 12 Aug 2024 18:55:35 +0200 Subject: [PATCH 61/65] chore: make coverage happy --- falcon/_typing_extensions.py | 2 +- falcon/hooks.py | 116 +++++++++++++++++------------------ 2 files changed, 56 insertions(+), 62 deletions(-) diff --git a/falcon/_typing_extensions.py b/falcon/_typing_extensions.py index 711d7ce2b..e87d3af0e 100644 --- a/falcon/_typing_extensions.py +++ b/falcon/_typing_extensions.py @@ -1,6 +1,6 @@ import sys -if sys.version_info < (3, 8): +if sys.version_info < (3, 8): # pragma: nocover from typing_extensions import Protocol as Protocol else: from typing import Protocol as Protocol diff --git a/falcon/hooks.py b/falcon/hooks.py index cad1e4ffa..9634e4a11 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -38,7 +38,7 @@ from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe -if TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: import falcon as wsgi from falcon import asgi from falcon.typing import AsyncResponderMethod @@ -46,61 +46,55 @@ from falcon.typing import Responder from falcon.typing import SyncResponderMethod + # TODO: if is_async is removed these protocol would no longer be needed, since + # ParamSpec could be used together with Concatenate to use a simple Callable + # to type the before and after functions. This approach was prototyped in + # https://github.com/falconry/falcon/pull/2234 + class SyncBeforeFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> None: ... + + class AsyncBeforeFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... + + BeforeFn = Union[SyncBeforeFn, AsyncBeforeFn] + + class SyncAfterFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> None: ... + + class AsyncAfterFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... -# TODO: if is_async is removed these protocol would no longer be needed, since -# ParamSpec could be used together with Concatenate to use a simple Callable -# to type the before and after functions. This approach was prototyped in -# https://github.com/falconry/falcon/pull/2234 -class SyncBeforeFn(Protocol): - def __call__( - self, - req: wsgi.Request, - resp: wsgi.Response, - resource: Resource, - params: Dict[str, Any], - *args: Any, - **kwargs: Any, - ) -> None: ... - - -class AsyncBeforeFn(Protocol): - def __call__( - self, - req: asgi.Request, - resp: asgi.Response, - resource: Resource, - params: Dict[str, Any], - *args: Any, - **kwargs: Any, - ) -> Awaitable[None]: ... - - -BeforeFn = Union[SyncBeforeFn, AsyncBeforeFn] - - -class SyncAfterFn(Protocol): - def __call__( - self, - req: wsgi.Request, - resp: wsgi.Response, - resource: Resource, - *args: Any, - **kwargs: Any, - ) -> None: ... - - -class AsyncAfterFn(Protocol): - def __call__( - self, - req: asgi.Request, - resp: asgi.Response, - resource: Resource, - *args: Any, - **kwargs: Any, - ) -> Awaitable[None]: ... - - -AfterFn = Union[SyncAfterFn, AsyncAfterFn] + AfterFn = Union[SyncAfterFn, AsyncAfterFn] _R = TypeVar('_R', bound=Union['Responder', 'Resource']) @@ -279,9 +273,9 @@ def _wrap_with_after( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = cast(AsyncAfterFn, _wrap_non_coroutine_unsafe(action)) + async_action = cast('AsyncAfterFn', _wrap_non_coroutine_unsafe(action)) else: - async_action = cast(AsyncAfterFn, action) + async_action = cast('AsyncAfterFn', action) async_responder = cast('AsyncResponderMethod', responder) @wraps(async_responder) @@ -300,7 +294,7 @@ async def do_after( do_after_responder = cast('AsyncResponderMethod', do_after) else: - sync_action = cast(SyncAfterFn, action) + sync_action = cast('SyncAfterFn', action) sync_responder = cast('SyncResponderMethod', responder) @wraps(sync_responder) @@ -351,9 +345,9 @@ def _wrap_with_before( # is actually covered, but coverage isn't tracking it for # some reason. if not is_async: # pragma: nocover - async_action = cast(AsyncBeforeFn, _wrap_non_coroutine_unsafe(action)) + async_action = cast('AsyncBeforeFn', _wrap_non_coroutine_unsafe(action)) else: - async_action = cast(AsyncBeforeFn, action) + async_action = cast('AsyncBeforeFn', action) async_responder = cast('AsyncResponderMethod', responder) @wraps(async_responder) @@ -372,7 +366,7 @@ async def do_before( do_before_responder = cast('AsyncResponderMethod', do_before) else: - sync_action = cast(SyncBeforeFn, action) + sync_action = cast('SyncBeforeFn', action) sync_responder = cast('SyncResponderMethod', responder) @wraps(sync_responder) From 3ce8b64b2b1205a668378f69a0efa1f5b08f1e9c Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Tue, 20 Aug 2024 22:49:46 +0200 Subject: [PATCH 62/65] refactor: remove support for python 3.7 --- falcon/_typing_extensions.py | 6 ------ falcon/hooks.py | 2 +- falcon/typing.py | 3 +-- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 falcon/_typing_extensions.py diff --git a/falcon/_typing_extensions.py b/falcon/_typing_extensions.py deleted file mode 100644 index e87d3af0e..000000000 --- a/falcon/_typing_extensions.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys - -if sys.version_info < (3, 8): # pragma: nocover - from typing_extensions import Protocol as Protocol -else: - from typing import Protocol as Protocol diff --git a/falcon/hooks.py b/falcon/hooks.py index 9634e4a11..1b2b2ea6d 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -27,13 +27,13 @@ cast, Dict, List, + Protocol, Tuple, TYPE_CHECKING, TypeVar, Union, ) -from falcon._typing_extensions import Protocol from falcon.constants import COMBINED_METHODS from falcon.util.misc import get_argnames from falcon.util.sync import _wrap_non_coroutine_unsafe diff --git a/falcon/typing.py b/falcon/typing.py index 8d2785853..1c92686c7 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -22,13 +22,12 @@ Dict, List, Pattern, + Protocol, Tuple, TYPE_CHECKING, Union, ) -from falcon._typing_extensions import Protocol - if TYPE_CHECKING: import falcon as wsgi from falcon import asgi From 55bf166895879ee98374bbc373baf534e71d9d24 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 21 Aug 2024 13:05:42 +0200 Subject: [PATCH 63/65] chore: apply review suggestions --- falcon/hooks.py | 102 +++++++++++++++++++++++++---------------------- falcon/typing.py | 5 +-- setup.cfg | 1 - 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/falcon/hooks.py b/falcon/hooks.py index 1b2b2ea6d..fb377e6c8 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -46,55 +46,61 @@ from falcon.typing import Responder from falcon.typing import SyncResponderMethod - # TODO: if is_async is removed these protocol would no longer be needed, since - # ParamSpec could be used together with Concatenate to use a simple Callable - # to type the before and after functions. This approach was prototyped in - # https://github.com/falconry/falcon/pull/2234 - class SyncBeforeFn(Protocol): - def __call__( - self, - req: wsgi.Request, - resp: wsgi.Response, - resource: Resource, - params: Dict[str, Any], - *args: Any, - **kwargs: Any, - ) -> None: ... - - class AsyncBeforeFn(Protocol): - def __call__( - self, - req: asgi.Request, - resp: asgi.Response, - resource: Resource, - params: Dict[str, Any], - *args: Any, - **kwargs: Any, - ) -> Awaitable[None]: ... - - BeforeFn = Union[SyncBeforeFn, AsyncBeforeFn] - - class SyncAfterFn(Protocol): - def __call__( - self, - req: wsgi.Request, - resp: wsgi.Response, - resource: Resource, - *args: Any, - **kwargs: Any, - ) -> None: ... - - class AsyncAfterFn(Protocol): - def __call__( - self, - req: asgi.Request, - resp: asgi.Response, - resource: Resource, - *args: Any, - **kwargs: Any, - ) -> Awaitable[None]: ... - AfterFn = Union[SyncAfterFn, AsyncAfterFn] +# TODO: if is_async is removed these protocol would no longer be needed, since +# ParamSpec could be used together with Concatenate to use a simple Callable +# to type the before and after functions. This approach was prototyped in +# https://github.com/falconry/falcon/pull/2234 +class SyncBeforeFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> None: ... + + +class AsyncBeforeFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + params: Dict[str, Any], + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... + + +BeforeFn = Union[SyncBeforeFn, AsyncBeforeFn] + + +class SyncAfterFn(Protocol): + def __call__( + self, + req: wsgi.Request, + resp: wsgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> None: ... + + +class AsyncAfterFn(Protocol): + def __call__( + self, + req: asgi.Request, + resp: asgi.Response, + resource: Resource, + *args: Any, + **kwargs: Any, + ) -> Awaitable[None]: ... + + +AfterFn = Union[SyncAfterFn, AsyncAfterFn] _R = TypeVar('_R', bound=Union['Responder', 'Resource']) diff --git a/falcon/typing.py b/falcon/typing.py index 1c92686c7..6bb5315fc 100644 --- a/falcon/typing.py +++ b/falcon/typing.py @@ -29,7 +29,6 @@ ) if TYPE_CHECKING: - import falcon as wsgi from falcon import asgi from falcon.request import Request from falcon.response import Response @@ -73,8 +72,8 @@ class SyncResponderMethod(Protocol): def __call__( self, resource: Resource, - req: wsgi.Request, - resp: wsgi.Response, + req: Request, + resp: Response, *args: Any, **kwargs: Any, ) -> None: ... diff --git a/setup.cfg b/setup.cfg index 5bfe8c99a..04a693b0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,7 +54,6 @@ include_package_data = True packages = find: python_requires = >=3.8 install_requires = - typing-extensions; python_version<"3.8" tests_require = testtools requests From e99582881f2ba0e8e3f235ee14b60bdd7073e692 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 21 Aug 2024 13:48:16 +0200 Subject: [PATCH 64/65] chore: additional ignore for coverage to better support typing --- .coveragerc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2c5041d7e..bce877f6a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,9 +7,10 @@ parallel = True [report] show_missing = True -exclude_lines = +# https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion +exclude_also = if TYPE_CHECKING: - if not TYPE_CHECKING: pragma: nocover - pragma: no cover pragma: no py39,py310 cover + @overload + class .*\bProtocol\): From efdf4dce4cca9c83eeffedea78fa3c859593c443 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Wed, 21 Aug 2024 14:44:40 +0200 Subject: [PATCH 65/65] chore: coverage again.. --- falcon/util/uri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon/util/uri.py b/falcon/util/uri.py index f9a772785..a2a324f02 100644 --- a/falcon/util/uri.py +++ b/falcon/util/uri.py @@ -554,7 +554,7 @@ def unquote_string(quoted: str) -> str: # TODO(vytas): Restructure this in favour of a cleaner way to hoist the pure # Cython functions into this module. -if not TYPE_CHECKING: +if not TYPE_CHECKING: # pragma: nocover if _cy_uri is not None: decode = _cy_uri.decode # NOQA parse_query_string = _cy_uri.parse_query_string # NOQA