From b444a265ec2f1f90925ebd2a7428ad454f909bc7 Mon Sep 17 00:00:00 2001 From: Federico Caselli Date: Fri, 18 Oct 2024 15:33:58 +0200 Subject: [PATCH] types: fixed type errors reported by pyright --- falcon/__init__.py | 12 ++-- falcon/_typing.py | 8 +-- falcon/app.py | 18 +++--- falcon/app_helpers.py | 25 ++------ falcon/asgi/app.py | 45 ++++++-------- falcon/asgi/response.py | 4 +- falcon/asgi/ws.py | 2 +- falcon/errors.py | 88 +++++++++++++-------------- falcon/inspect.py | 2 +- falcon/media/base.py | 4 +- falcon/media/json.py | 6 +- falcon/media/msgpack.py | 2 +- falcon/media/multipart.py | 2 +- falcon/media/validators/jsonschema.py | 32 ++++++---- falcon/response_helpers.py | 3 +- falcon/testing/helpers.py | 4 +- falcon/testing/test_case.py | 4 +- pyproject.toml | 7 +++ 18 files changed, 131 insertions(+), 137 deletions(-) diff --git a/falcon/__init__.py b/falcon/__init__.py index cdc19f4dc..55ea9cde4 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -23,6 +23,8 @@ app = falcon.App() """ +import logging as _logging + __all__ = ( # API interface 'API', @@ -407,11 +409,6 @@ from falcon.hooks import before from falcon.http_error import HTTPError from falcon.http_status import HTTPStatus - -# NOTE(jkmnt): Moved logger to leaf module to avoid possible circular imports. -# the _logging symbol is reexported too - maybe it was used by test or smth. -from falcon.logger import _logger -from falcon.logger import logging as _logging from falcon.middleware import CORSMiddleware from falcon.redirects import HTTPFound from falcon.redirects import HTTPMovedPermanently @@ -644,3 +641,8 @@ # Package version from falcon.version import __version__ # NOQA: F401 + +# NOTE(kgriffs): Only to be used internally on the rare occasion that we +# need to log something that we can't communicate any other way. +_logger = _logging.getLogger('falcon') +_logger.addHandler(_logging.NullHandler()) diff --git a/falcon/_typing.py b/falcon/_typing.py index 946bf2c02..648bb8c8d 100644 --- a/falcon/_typing.py +++ b/falcon/_typing.py @@ -32,7 +32,6 @@ Optional, Pattern, Protocol, - Sequence, Tuple, TYPE_CHECKING, TypeVar, @@ -105,9 +104,6 @@ async def __call__( HeaderMapping = Mapping[str, str] HeaderIter = Iterable[Tuple[str, str]] HeaderArg = Union[HeaderMapping, HeaderIter] - -NarrowHeaderArg = Union[Mapping[str, str], Sequence[Tuple[str, str]]] - ResponseStatus = Union[http.HTTPStatus, str, int] StoreArg = Optional[Dict[str, Any]] Resource = object @@ -168,8 +164,8 @@ async def __call__( AsgiProcessResponseMethod = Callable[ ['AsgiRequest', 'AsgiResponse', Resource, bool], Awaitable[None] ] -AsgiProcessStartupMethod = Callable[[Dict[str, Any], 'AsgiEvent'], Awaitable[None]] -AsgiProcessShutdownMethod = Callable[[Dict[str, Any], 'AsgiEvent'], Awaitable[None]] +# AsgiProcessStartupMethod = Callable[[Dict[str, Any], 'AsgiEvent'], Awaitable[None]] +# AsgiProcessShutdownMethod = Callable[[Dict[str, Any], 'AsgiEvent'], Awaitable[None]] AsgiProcessRequestWsMethod = Callable[['AsgiRequest', 'WebSocket'], Awaitable[None]] AsgiProcessResourceWsMethod = Callable[ diff --git a/falcon/app.py b/falcon/app.py index d48243a11..42e1ef327 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -437,7 +437,7 @@ def __call__( # noqa: C901 break if not resp.complete: - responder(req, resp, **params) + responder(req, resp, **params) # pyright: ignore[reportPossiblyUnboundVariable] req_succeeded = True except Exception as ex: @@ -1071,7 +1071,7 @@ def _get_responder( if resource is not None: try: - responder = method_map[method] + responder = method_map[method] # pyright: ignore[reportPossiblyUnboundVariable] except KeyError: # NOTE(kgriffs): Dirty hack! We use __class__ here to avoid # binding self to the default responder method. We could @@ -1094,7 +1094,7 @@ def _get_responder( else: responder = self.__class__._default_responder_path_not_found - return (responder, params, resource, uri_template) + return (responder, params, resource, uri_template) # pyright: ignore[reportPossiblyUnboundVariable] def _compose_status_response( self, req: Request, resp: Response, http_status: HTTPStatus @@ -1258,17 +1258,21 @@ def _get_body( self._STREAM_BLOCK_SIZE, ) else: - iterable = cast(Iterable[bytes], stream) + iterable = stream - return iterable, None + return iterable, None # pyright: ignore[reportReturnType] return [], 0 def _update_sink_and_static_routes(self) -> None: if self._sink_before_static_route: - self._sink_and_static_routes = (*self._sinks, *self._static_routes) + self._sink_and_static_routes = tuple(self._sinks) + tuple( + self._static_routes + ) else: - self._sink_and_static_routes = (*self._static_routes, *self._sinks) + self._sink_and_static_routes = tuple(self._static_routes) + tuple( + self._sinks + ) # TODO(myusko): This class is a compatibility alias, and should be removed diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 5972054ed..3b6af0cd9 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -18,9 +18,7 @@ from inspect import iscoroutinefunction from typing import ( - Any, - Awaitable, - Callable, + IO, Iterable, List, Literal, @@ -45,7 +43,6 @@ from falcon.errors import HTTPError from falcon.request import Request from falcon.response import Response -from falcon.typing import ReadableIO from falcon.util.sync import _wrap_non_coroutine_unsafe __all__ = ( @@ -379,7 +376,7 @@ class CloseableStreamIterator: block_size (int): Number of bytes to read per iteration. """ - def __init__(self, stream: ReadableIO, block_size: int) -> None: + def __init__(self, stream: IO[bytes], block_size: int) -> None: self._stream = stream self._block_size = block_size @@ -395,17 +392,7 @@ def __next__(self) -> bytes: return data def close(self) -> None: - close_maybe(self._stream) - - -# TODO(jkmnt): Move these to some other module, they don't belong here -def close_maybe(stream: Any) -> None: - close: Optional[Callable[[], None]] = getattr(stream, 'close', None) - if close: - close() - - -async def async_close_maybe(stream: Any) -> None: - close: Optional[Callable[[], Awaitable[None]]] = getattr(stream, 'close', None) - if close: - await close() + try: + self._stream.close() + except (AttributeError, TypeError): + pass diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index 0613032de..a329d7f28 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -37,13 +37,12 @@ Union, ) +from falcon import _logger from falcon import constants from falcon import responders from falcon import routing from falcon._typing import _UNSET from falcon._typing import AsgiErrorHandler -from falcon._typing import AsgiProcessShutdownMethod -from falcon._typing import AsgiProcessStartupMethod from falcon._typing import AsgiReceive from falcon._typing import AsgiResponderCallable from falcon._typing import AsgiResponderWsCallable @@ -51,7 +50,6 @@ from falcon._typing import AsgiSinkCallable from falcon._typing import SinkPrefix import falcon.app -from falcon.app_helpers import async_close_maybe from falcon.app_helpers import AsyncPreparedMiddlewareResult from falcon.app_helpers import AsyncPreparedMiddlewareWsResult from falcon.app_helpers import prepare_middleware @@ -66,7 +64,6 @@ from falcon.errors import WebSocketDisconnected from falcon.http_error import HTTPError from falcon.http_status import HTTPStatus -from falcon.logger import _logger from falcon.media.multipart import MultipartFormHandler from falcon.util import get_argnames from falcon.util.misc import is_python_func @@ -343,10 +340,10 @@ async def process_resource_ws( # without having to import falcon.asgi. _ASGI: ClassVar[bool] = True - _default_responder_bad_request: ClassVar[AsgiResponderCallable] = ( + _default_responder_bad_request: ClassVar[AsgiResponderCallable] = ( # pyright: ignore[reportIncompatibleVariableOverride]# noqa: E501 responders.bad_request_async # type: ignore[assignment] ) - _default_responder_path_not_found: ClassVar[AsgiResponderCallable] = ( + _default_responder_path_not_found: ClassVar[AsgiResponderCallable] = ( # pyright: ignore[reportIncompatibleVariableOverride] # noqa: E501 responders.path_not_found_async # type: ignore[assignment] ) @@ -359,8 +356,8 @@ async def process_resource_ws( _error_handlers: Dict[Type[BaseException], AsgiErrorHandler] # type: ignore[assignment] _middleware: AsyncPreparedMiddlewareResult # type: ignore[assignment] _middleware_ws: AsyncPreparedMiddlewareWsResult - _request_type: Type[Request] - _response_type: Type[Response] + _request_type: Type[Request] # pyright: ignore[reportIncompatibleVariableOverride] + _response_type: Type[Response] # pyright: ignore[reportIncompatibleVariableOverride] ws_options: WebSocketOptions """A set of behavioral options related to WebSocket connections. @@ -526,7 +523,7 @@ async def __call__( # type: ignore[override] # noqa: C901 break if not resp.complete: - await responder(req, resp, **params) + await responder(req, resp, **params) # pyright: ignore[reportPossiblyUnboundVariable] req_succeeded = True @@ -773,13 +770,10 @@ async def watch_disconnect() -> None: # (c) async iterator # - read_meth: Optional[Callable[[int], Awaitable[bytes]]] = getattr( - stream, 'read', None - ) - if read_meth: + if hasattr(stream, 'read'): try: while True: - data = await read_meth(self._STREAM_BLOCK_SIZE) + data = await stream.read(self._STREAM_BLOCK_SIZE) # pyright: ignore[reportAttributeAccessIssue] if data == b'': break else: @@ -793,7 +787,8 @@ async def watch_disconnect() -> None: } ) finally: - await async_close_maybe(stream) + if hasattr(stream, 'close'): + await stream.close() # pyright: ignore[reportAttributeAccessIssue] else: # NOTE(kgriffs): Works for both async generators and iterators try: @@ -832,7 +827,8 @@ async def watch_disconnect() -> None: # NOTE(vytas): This could be DRYed with the above identical # twoliner in a one large block, but OTOH we would be # unable to reuse the current try.. except. - await async_close_maybe(stream) + if hasattr(stream, 'close'): + await stream.close() # pyright: ignore await send(_EVT_RESP_EOF) @@ -1009,8 +1005,7 @@ async def handle(req, resp, ex, params): 'The handler must be an awaitable coroutine function in order ' 'to be used safely with an ASGI app.' ) - assert handler - handler_callable: AsgiErrorHandler = handler + handler_callable: AsgiErrorHandler = handler # pyright: ignore[reportAssignmentType] exception_tuple: Tuple[type[BaseException], ...] try: @@ -1082,12 +1077,9 @@ async def _call_lifespan_handlers( return for handler in self._unprepared_middleware: - process_startup: Optional[AsgiProcessStartupMethod] = getattr( - handler, 'process_startup', None - ) - if process_startup: + if hasattr(handler, 'process_startup'): try: - await process_startup(scope, event) + await handler.process_startup(scope, event) # pyright: ignore[reportAttributeAccessIssue] except Exception: await send( { @@ -1101,12 +1093,9 @@ async def _call_lifespan_handlers( elif event['type'] == 'lifespan.shutdown': for handler in reversed(self._unprepared_middleware): - process_shutdown: Optional[AsgiProcessShutdownMethod] = getattr( - handler, 'process_shutdown', None - ) - if process_shutdown: + if hasattr(handler, 'process_shutdown'): try: - await process_shutdown(scope, event) + await handler.process_shutdown(scope, event) # pyright: ignore[reportAttributeAccessIssue] except Exception: await send( { diff --git a/falcon/asgi/response.py b/falcon/asgi/response.py index 1b5717444..0561c9b0c 100644 --- a/falcon/asgi/response.py +++ b/falcon/asgi/response.py @@ -146,7 +146,7 @@ async def emitter(): def sse(self, value: Optional[SSEEmitter]) -> None: self._sse = value - def set_stream( + def set_stream( # pyright: ignore[reportIncompatibleMethodOverride] self, stream: Union[AsyncReadableIO, AsyncIterator[bytes]], # type: ignore[override] content_length: int, @@ -175,7 +175,7 @@ def set_stream( Content-Length header in the response. """ - self.stream = stream + self.stream = stream # pyright: ignore[reportIncompatibleVariableOverride] # PERF(kgriffs): Set directly rather than incur the overhead of # the self.content_length property. diff --git a/falcon/asgi/ws.py b/falcon/asgi/ws.py index 6f8ba7191..709a13d2d 100644 --- a/falcon/asgi/ws.py +++ b/falcon/asgi/ws.py @@ -623,7 +623,7 @@ class WebSocketOptions: @classmethod def _init_default_close_reasons(cls) -> Dict[int, str]: - reasons: dict[int, str] = dict(cls._STANDARD_CLOSE_REASONS) + reasons = dict(cls._STANDARD_CLOSE_REASONS) for status_constant in dir(status_codes): if 'HTTP_100' <= status_constant < 'HTTP_599': status_line = getattr(status_codes, status_constant) diff --git a/falcon/errors.py b/falcon/errors.py index 6c647ba05..63f384a9a 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -45,7 +45,7 @@ def on_get(self, req, resp): from falcon.util.misc import dt_to_http if TYPE_CHECKING: - from falcon._typing import NarrowHeaderArg + from falcon._typing import HeaderArg from falcon.typing import Headers @@ -270,7 +270,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -350,7 +350,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, challenges: Optional[Iterable[str]] = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -425,7 +425,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -493,7 +493,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -614,7 +614,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) @@ -682,7 +682,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -752,7 +752,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -828,7 +828,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -889,7 +889,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -951,7 +951,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1026,7 +1026,7 @@ def __init__( title: Optional[str] = None, description: Optional[str] = None, retry_after: RetryAfter = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -1104,7 +1104,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1166,7 +1166,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1242,7 +1242,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) @@ -1309,7 +1309,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1368,7 +1368,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1426,7 +1426,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1492,7 +1492,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1563,7 +1563,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, retry_after: RetryAfter = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -1629,7 +1629,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1701,7 +1701,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1759,7 +1759,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1824,7 +1824,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1882,7 +1882,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1956,7 +1956,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, retry_after: RetryAfter = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -2016,7 +2016,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2080,7 +2080,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2142,7 +2142,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2201,7 +2201,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2272,7 +2272,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2328,7 +2328,7 @@ def __init__( msg: str, header_name: str, *, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): description = 'The value provided for the "{0}" header is invalid. {1}' @@ -2384,7 +2384,7 @@ def __init__( self, header_name: str, *, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): description = 'The "{0}" header is required.' @@ -2444,7 +2444,7 @@ def __init__( msg: str, param_name: str, *, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is invalid. {1}' @@ -2502,7 +2502,7 @@ def __init__( self, param_name: str, *, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is required.' @@ -2603,7 +2603,7 @@ class MediaMalformedError(HTTPBadRequest): def __init__( self, media_type: str, - **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments], + **kwargs: Union[HeaderArg, HTTPErrorKeywordArguments], ): super().__init__( title='Invalid {0}'.format(media_type), @@ -2620,7 +2620,7 @@ def description(self) -> Optional[str]: return msg @description.setter - def description(self, value: str) -> None: + def description(self, value: str) -> None: # pyright: ignore[reportIncompatibleVariableOverride] pass @@ -2674,7 +2674,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[NarrowHeaderArg] = None, + headers: Optional[HeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -2703,13 +2703,13 @@ class MultipartParseError(MediaMalformedError): """ # NOTE(caselit): remove the description @property in MediaMalformedError - description = None # pyright: ignore[reportAssignmentType, reportGeneralTypeIssues] + description = None # pyright: ignore def __init__( self, *, description: Optional[str] = None, - **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments], + **kwargs: Union[HeaderArg, HTTPErrorKeywordArguments], ) -> None: HTTPBadRequest.__init__( self, @@ -2724,19 +2724,19 @@ def __init__( # ----------------------------------------------------------------------------- -def _load_headers(headers: Optional[NarrowHeaderArg]) -> Headers: +def _load_headers(headers: Optional[HeaderArg]) -> Headers: """Transform the headers to dict.""" if headers is None: return {} if isinstance(headers, dict): - return headers + return headers # pyright: ignore[reportReturnType] return dict(headers) def _parse_retry_after( - headers: Optional[NarrowHeaderArg], + headers: Optional[HeaderArg], retry_after: RetryAfter, -) -> Optional[NarrowHeaderArg]: +) -> Optional[HeaderArg]: """Set the Retry-After to the headers when required.""" if retry_after is None: return headers diff --git a/falcon/inspect.py b/falcon/inspect.py index 7cb749d0d..80b3003e0 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -250,7 +250,7 @@ def _traverse(roots: List[CompiledRouterNode], parent: str) -> None: method_info = RouteMethodInfo( method, source_info, - getattr(real_func, '__name__', '?'), + real_func.__name__, # pyright: ignore[reportAttributeAccessIssue] internal, ) methods.append(method_info) diff --git a/falcon/media/base.py b/falcon/media/base.py index daa8e1040..0d80611f3 100644 --- a/falcon/media/base.py +++ b/falcon/media/base.py @@ -28,7 +28,7 @@ class BaseHandler(metaclass=abc.ABCMeta): """Override to provide a synchronous deserialization method that takes a byte string.""" - def serialize(self, media: object, content_type: Optional[str] = None) -> bytes: + def serialize(self, media: object, content_type: str) -> bytes: """Serialize the media object on a :any:`falcon.Response`. By default, this method raises an instance of @@ -51,7 +51,7 @@ def serialize(self, media: object, content_type: Optional[str] = None) -> bytes: Returns: bytes: The resulting serialized bytes from the input object. """ - if content_type is not None and MEDIA_JSON in content_type: + if MEDIA_JSON in content_type: raise NotImplementedError( 'The JSON media handler requires the sync interface to be ' "implemented even in ASGI applications, because it's used " diff --git a/falcon/media/json.py b/falcon/media/json.py index 502be0126..e1df2726c 100644 --- a/falcon/media/json.py +++ b/falcon/media/json.py @@ -165,16 +165,16 @@ def __init__( # proper serialize implementation. result = self._dumps({'message': 'Hello World'}) if isinstance(result, str): - self.serialize = self._serialize_s # type: ignore[method-assign] + self.serialize = s_s = self._serialize_s # type: ignore[method-assign] self.serialize_async = self._serialize_async_s # type: ignore[method-assign] else: - self.serialize = self._serialize_b # type: ignore[method-assign] + self.serialize = s_s = self._serialize_b # type: ignore[method-assign] self.serialize_async = self._serialize_async_b # type: ignore[method-assign] # NOTE(kgriffs): To be safe, only enable the optimized protocol when # not subclassed. if type(self) is JSONHandler: - self._serialize_sync = self.serialize + self._serialize_sync = s_s self._deserialize_sync = self._deserialize def _deserialize(self, data: bytes) -> Any: diff --git a/falcon/media/msgpack.py b/falcon/media/msgpack.py index 95931811d..5b8c587c9 100644 --- a/falcon/media/msgpack.py +++ b/falcon/media/msgpack.py @@ -72,7 +72,7 @@ async def deserialize_async( ) -> Any: return self._deserialize(await stream.read()) - def serialize(self, media: Any, content_type: Optional[str] = None) -> bytes: + def serialize(self, media: Any, content_type: Optional[str]) -> bytes: return self._pack(media) async def serialize_async(self, media: Any, content_type: Optional[str]) -> bytes: diff --git a/falcon/media/multipart.py b/falcon/media/multipart.py index cf1aba289..99cd8785e 100644 --- a/falcon/media/multipart.py +++ b/falcon/media/multipart.py @@ -549,7 +549,7 @@ async def deserialize_async( stream, content_type, content_length, form_cls=self._ASGI_MULTIPART_FORM ) - def serialize(self, media: object, content_type: Optional[str] = None) -> NoReturn: + def serialize(self, media: object, content_type: str) -> NoReturn: raise NotImplementedError('multipart form serialization unsupported') diff --git a/falcon/media/validators/jsonschema.py b/falcon/media/validators/jsonschema.py index 69be06345..295be7b3c 100644 --- a/falcon/media/validators/jsonschema.py +++ b/falcon/media/validators/jsonschema.py @@ -102,10 +102,12 @@ def wrapper( ) -> Any: if req_schema is not None: try: - jsonschema.validate( - req.media, req_schema, format_checker=jsonschema.FormatChecker() + jsonschema.validate( # pyright: ignore[reportPossiblyUnboundVariable] + req.media, + req_schema, + format_checker=jsonschema.FormatChecker(), # pyright: ignore[reportPossiblyUnboundVariable] ) - except jsonschema.ValidationError as ex: + except jsonschema.ValidationError as ex: # pyright: ignore[reportPossiblyUnboundVariable] raise falcon.MediaValidationError( title='Request data failed validation', description=ex.message ) from ex @@ -114,10 +116,12 @@ def wrapper( if resp_schema is not None: try: - jsonschema.validate( - resp.media, resp_schema, format_checker=jsonschema.FormatChecker() + jsonschema.validate( # pyright: ignore[reportPossiblyUnboundVariable] + resp.media, + resp_schema, + format_checker=jsonschema.FormatChecker(), # pyright: ignore[reportPossiblyUnboundVariable] ) - except jsonschema.ValidationError as ex: + except jsonschema.ValidationError as ex: # pyright: ignore[reportPossiblyUnboundVariable] raise falcon.HTTPInternalServerError( title='Response data failed validation' # Do not return 'e.message' in the response to @@ -141,10 +145,12 @@ async def wrapper( m = await req.get_media() try: - jsonschema.validate( - m, req_schema, format_checker=jsonschema.FormatChecker() + jsonschema.validate( # pyright: ignore[reportPossiblyUnboundVariable] + m, + req_schema, + format_checker=jsonschema.FormatChecker(), # pyright: ignore[reportPossiblyUnboundVariable] ) - except jsonschema.ValidationError as ex: + except jsonschema.ValidationError as ex: # pyright: ignore[reportPossiblyUnboundVariable] raise falcon.MediaValidationError( title='Request data failed validation', description=ex.message ) from ex @@ -153,10 +159,12 @@ async def wrapper( if resp_schema is not None: try: - jsonschema.validate( - resp.media, resp_schema, format_checker=jsonschema.FormatChecker() + jsonschema.validate( # pyright: ignore[reportPossiblyUnboundVariable] + resp.media, + resp_schema, + format_checker=jsonschema.FormatChecker(), # pyright: ignore[reportPossiblyUnboundVariable] ) - except jsonschema.ValidationError as ex: + except jsonschema.ValidationError as ex: # pyright: ignore[reportPossiblyUnboundVariable] raise falcon.HTTPInternalServerError( title='Response data failed validation' # Do not return 'e.message' in the response to diff --git a/falcon/response_helpers.py b/falcon/response_helpers.py index 1564fcc71..330764892 100644 --- a/falcon/response_helpers.py +++ b/falcon/response_helpers.py @@ -153,4 +153,5 @@ def _headers_to_items(headers: HeaderArg) -> HeaderIter: header_items: Optional[Callable[[], HeaderIter]] = getattr(headers, 'items', None) if callable(header_items): return header_items() - return headers # type: ignore[return-value] + else: + return headers # type: ignore[return-value] diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 3b0ef50fe..dec8520c1 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -59,7 +59,6 @@ from falcon._typing import CookieArg from falcon._typing import HeaderArg from falcon._typing import ResponseStatus -from falcon.app_helpers import close_maybe import falcon.asgi from falcon.asgi_spec import AsgiEvent from falcon.asgi_spec import EventType @@ -1410,7 +1409,8 @@ def wrapper() -> Iterator[bytes]: for item in iterable: yield item finally: - close_maybe(iterable) + if hasattr(iterable, 'close'): + iterable.close() # pyright: ignore[reportAttributeAccessIssue] wrapped = wrapper() head: Tuple[bytes, ...] diff --git a/falcon/testing/test_case.py b/falcon/testing/test_case.py index 632b662b7..8b2dbe1db 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -19,7 +19,7 @@ """ try: - import testtools as unittest + import testtools as unittest # pyright: ignore[reportMissingImports] except ImportError: # pragma: nocover import unittest @@ -31,7 +31,7 @@ from falcon.testing.client import TestClient -class TestCase(unittest.TestCase, TestClient): +class TestCase(unittest.TestCase, TestClient): # pyright: ignore[reportGeneralTypeIssues] """Extends :mod:`unittest` to support WSGI/ASGI functional testing. Note: diff --git a/pyproject.toml b/pyproject.toml index 4fe30640a..3b37b0950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,13 @@ exclude = ["examples", "tests"] ] ignore_missing_imports = true +[tool.pyright] + exclude = [ + "falcon/bench", + "falcon/cmd", + ] + reportUnnecessaryTypeComment = true # doesn't seem to work. https://github.com/microsoft/pyright/issues/2839#issuecomment-2421779145 + [tool.towncrier] package = "falcon" package_dir = ""