From c9eb608a524854da9736b29246be55f1badbd6c1 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 15 Oct 2024 19:40:58 +0300 Subject: [PATCH 01/10] small changes --- falcon/__init__.py | 10 ++++------ falcon/_typing.py | 4 +++- falcon/app_helpers.py | 21 +++++++++++++++------ falcon/hooks.py | 8 ++++---- falcon/inspect.py | 2 +- falcon/testing/helpers.py | 10 ++++------ 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/falcon/__init__.py b/falcon/__init__.py index 55ea9cde4..67189d73d 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -23,8 +23,6 @@ app = falcon.App() """ -import logging as _logging - __all__ = ( # API interface 'API', @@ -639,10 +637,10 @@ from falcon.util import wrap_sync_to_async from falcon.util import wrap_sync_to_async_unsafe +# 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, logging as _logging + # 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 d82a5bac5..7c6b0b498 100644 --- a/falcon/_typing.py +++ b/falcon/_typing.py @@ -164,6 +164,9 @@ 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]] + AsgiProcessRequestWsMethod = Callable[['AsgiRequest', 'WebSocket'], Awaitable[None]] AsgiProcessResourceWsMethod = Callable[ ['AsgiRequest', 'WebSocket', Resource, Dict[str, Any]], Awaitable[None] @@ -173,7 +176,6 @@ async def __call__( Tuple[Callable[[], Awaitable[None]], Literal[True]], ] - # Routing MethodDict = Union[ diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 1248d280b..5b09fa893 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -17,7 +17,7 @@ from __future__ import annotations from inspect import iscoroutinefunction -from typing import IO, Iterable, List, Literal, Optional, overload, Tuple, Union +from typing import Any, Awaitable, Callable, Iterable, List, Literal, Optional, overload, Tuple, Union from falcon import util from falcon._typing import AsgiProcessRequestMethod as APRequest @@ -34,6 +34,7 @@ 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__ = ( @@ -367,7 +368,7 @@ class CloseableStreamIterator: block_size (int): Number of bytes to read per iteration. """ - def __init__(self, stream: IO[bytes], block_size: int) -> None: + def __init__(self, stream: ReadableIO, block_size: int) -> None: self._stream = stream self._block_size = block_size @@ -383,7 +384,15 @@ def __next__(self) -> bytes: return data def close(self) -> None: - try: - self._stream.close() - except (AttributeError, TypeError): - pass + close_maybe(self._stream) + + +def close_maybe(stream: Any): + close: Callable[[], None] | None = getattr(stream, 'close', None) + if close: + close() + +async def async_close_maybe(stream: Any): + close: Callable[[], Awaitable[None]] | None = getattr(stream, 'close', None) + if close: + await close() \ No newline at end of file diff --git a/falcon/hooks.py b/falcon/hooks.py index dc2afbd0f..116b65e84 100644 --- a/falcon/hooks.py +++ b/falcon/hooks.py @@ -233,7 +233,7 @@ def _wrap_with_after( async_responder = cast('AsgiResponderMethod', responder) @wraps(async_responder) - async def do_after( + async def do_after_async( self: Resource, req: asgi.Request, resp: asgi.Response, @@ -246,7 +246,7 @@ 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('AsgiResponderMethod', do_after) + do_after_responder = cast('AsgiResponderMethod', do_after_async) else: sync_action = cast('SyncAfterFn', action) sync_responder = cast('ResponderMethod', responder) @@ -294,7 +294,7 @@ def _wrap_with_before( async_responder = cast('AsgiResponderMethod', responder) @wraps(async_responder) - async def do_before( + async def do_before_async( self: Resource, req: asgi.Request, resp: asgi.Response, @@ -307,7 +307,7 @@ 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('AsgiResponderMethod', do_before) + do_before_responder = cast('AsgiResponderMethod', do_before_async) else: sync_action = cast('SyncBeforeFn', action) sync_responder = cast('ResponderMethod', responder) diff --git a/falcon/inspect.py b/falcon/inspect.py index 00b840d68..06dbd3185 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -248,7 +248,7 @@ def _traverse(roots: List[CompiledRouterNode], parent: str) -> None: 'info will always be a string' ) method_info = RouteMethodInfo( - method, source_info, real_func.__name__, internal + method, source_info, getattr(real_func, '__name__', '?'), internal ) methods.append(method_info) source_info, class_name = _get_source_info_and_name(root.resource) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 97392d57a..c8d192f8f 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -69,6 +69,7 @@ from falcon.util import code_to_http_status from falcon.util import uri from falcon.util.mediatypes import parse_header +from falcon.app_helpers import close_maybe # NOTE(kgriffs): Changed in 3.0 from 'curl/7.24.0 (x86_64-apple-darwin12.0)' DEFAULT_UA = 'falcon-client/' + falcon.__version__ @@ -162,15 +163,13 @@ def __init__( elif not isinstance(body, bytes): body = body.encode() - body = memoryview(body) - if disconnect_at is None: disconnect_at = time.time() + 30 if chunk_size is None: chunk_size = 4096 - self._body: Optional[memoryview] = body + self._body = memoryview(body) self._chunk_size = chunk_size self._emit_empty_chunks = True self._disconnect_at = disconnect_at @@ -1411,8 +1410,7 @@ def wrapper() -> Iterator[bytes]: for item in iterable: yield item finally: - if hasattr(iterable, 'close'): - iterable.close() + close_maybe(iterable) wrapped = wrapper() head: Tuple[bytes, ...] @@ -1528,7 +1526,7 @@ def _fixup_http_version(http_version: str) -> str: def _make_cookie_values(cookies: CookieArg) -> str: return '; '.join( [ - '{}={}'.format(key, cookie.value if hasattr(cookie, 'value') else cookie) + '{}={}'.format(key, getattr(cookie, 'value', cookie)) for key, cookie in cookies.items() ] ) From 52606467c3942a46162a82b65a9f0c2c2db1e341 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 15 Oct 2024 19:43:07 +0300 Subject: [PATCH 02/10] few errors less --- falcon/asgi/app.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index f3b637802..f9cd1f271 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -60,6 +60,7 @@ from falcon.errors import CompatibilityError from falcon.errors import HTTPBadRequest from falcon.errors import WebSocketDisconnected +from falcon.errors import HTTPInternalServerError from falcon.http_error import HTTPError from falcon.http_status import HTTPStatus from falcon.media.multipart import MultipartFormHandler @@ -68,6 +69,7 @@ from falcon.util.sync import _should_wrap_non_coroutines from falcon.util.sync import _wrap_non_coroutine_unsafe from falcon.util.sync import wrap_sync_to_async +from falcon.logger import _logger from ._asgi_helpers import _validate_asgi_scope from ._asgi_helpers import _wrap_asgi_coroutine_func @@ -1185,7 +1187,7 @@ async def _http_status_handler( # type: ignore[override] self._compose_status_response(req, resp, status) elif ws: code = http_status_to_ws_code(status.status_code) - falcon._logger.error( + _logger.error( '[FALCON] HTTPStatus %s raised while handling WebSocket. ' 'Closing with code %s', status, @@ -1207,7 +1209,7 @@ async def _http_error_handler( # type: ignore[override] self._compose_error_response(req, resp, error) elif ws: code = http_status_to_ws_code(error.status_code) - falcon._logger.error( + _logger.error( '[FALCON] HTTPError %s raised while handling WebSocket. ' 'Closing with code %s', error, @@ -1225,10 +1227,10 @@ async def _python_error_handler( # type: ignore[override] params: Dict[str, Any], ws: Optional[WebSocket] = None, ) -> None: - falcon._logger.error('[FALCON] Unhandled exception in ASGI app', exc_info=error) + _logger.error('[FALCON] Unhandled exception in ASGI app', exc_info=error) if resp: - self._compose_error_response(req, resp, falcon.HTTPInternalServerError()) + self._compose_error_response(req, resp, HTTPInternalServerError()) elif ws: await self._ws_cleanup_on_error(ws) else: @@ -1244,7 +1246,7 @@ async def _ws_disconnected_error_handler( ) -> None: assert resp is None assert ws is not None - falcon._logger.debug( + _logger.debug( '[FALCON] WebSocket client disconnected with code %i', error.code ) await self._ws_cleanup_on_error(ws) @@ -1323,7 +1325,7 @@ async def _ws_cleanup_on_error(self, ws: WebSocket) -> None: if 'invalid close code' in str(ex).lower(): await ws.close(_FALLBACK_WS_ERROR_CODE) else: - falcon._logger.warning( + _logger.warning( ( '[FALCON] Attempt to close web connection cleanly ' 'failed due to raised error.' From f992c0c94d03d8cec03bec4c20e1d693556d114a Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 15 Oct 2024 19:53:02 +0300 Subject: [PATCH 03/10] - another few errors gone --- falcon/asgi/app.py | 19 +++++++++++-------- falcon/asgi/ws.py | 2 +- falcon/routing/compiled.py | 4 +++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index f9cd1f271..4dade057f 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -42,6 +42,8 @@ from falcon import routing from falcon._typing import _UNSET from falcon._typing import AsgiErrorHandler +from falcon._typing import AsgiProcessStartupMethod +from falcon._typing import AsgiProcessShutdownMethod from falcon._typing import AsgiReceive from falcon._typing import AsgiResponderCallable from falcon._typing import AsgiResponderWsCallable @@ -53,6 +55,7 @@ from falcon.app_helpers import AsyncPreparedMiddlewareWsResult from falcon.app_helpers import prepare_middleware from falcon.app_helpers import prepare_middleware_ws +from falcon.app_helpers import async_close_maybe from falcon.asgi_spec import AsgiSendMsg from falcon.asgi_spec import EventType from falcon.asgi_spec import WSCloseCode @@ -787,8 +790,7 @@ async def watch_disconnect() -> None: } ) finally: - if hasattr(stream, 'close'): - await stream.close() + await async_close_maybe(stream) else: # NOTE(kgriffs): Works for both async generators and iterators try: @@ -827,8 +829,7 @@ 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. - if hasattr(stream, 'close'): - await stream.close() + await async_close_maybe(stream) await send(_EVT_RESP_EOF) @@ -1077,9 +1078,10 @@ async def _call_lifespan_handlers( return for handler in self._unprepared_middleware: - if hasattr(handler, 'process_startup'): + process_startup: AsgiProcessStartupMethod | None = getattr(handler, 'process_startup', None) + if process_startup: try: - await handler.process_startup(scope, event) + await process_startup(scope, event) except Exception: await send( { @@ -1093,9 +1095,10 @@ async def _call_lifespan_handlers( elif event['type'] == 'lifespan.shutdown': for handler in reversed(self._unprepared_middleware): - if hasattr(handler, 'process_shutdown'): + process_shutdown = getattr(handler, 'process_shutdown', None) + if process_shutdown: try: - await handler.process_shutdown(scope, event) + await process_shutdown(scope, event) except Exception: await send( { diff --git a/falcon/asgi/ws.py b/falcon/asgi/ws.py index 03800682f..372a3f5b8 100644 --- a/falcon/asgi/ws.py +++ b/falcon/asgi/ws.py @@ -628,7 +628,7 @@ class WebSocketOptions: @classmethod def _init_default_close_reasons(cls) -> Dict[int, str]: - reasons = dict(cls._STANDARD_CLOSE_REASONS) + reasons: dict[int, str] = 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/routing/compiled.py b/falcon/routing/compiled.py index 836288780..961233d81 100644 --- a/falcon/routing/compiled.py +++ b/falcon/routing/compiled.py @@ -570,6 +570,8 @@ def _generate_ast( # noqa: C901 # return the relevant information. resource_idx = len(return_values) return_values.append(node) + else: + resource_idx = None assert not (consume_multiple_segments and node.children) @@ -583,7 +585,7 @@ def _generate_ast( # noqa: C901 fast_return, ) - if node.resource is None: + if resource_idx is None: if fast_return: parent.append_child(_CxReturnNone()) else: From 885cb16ed9505211133883cc63cac6a7343b249a Mon Sep 17 00:00:00 2001 From: jkmnt Date: Tue, 15 Oct 2024 23:05:30 +0300 Subject: [PATCH 04/10] no errors in basic mode --- falcon/_typing.py | 5 ++- falcon/app.py | 13 ++++-- falcon/app_helpers.py | 6 +-- falcon/asgi/app.py | 6 ++- falcon/asgi/ws.py | 9 +--- falcon/errors.py | 84 +++++++++++++++++++------------------- falcon/media/base.py | 4 +- falcon/response.py | 11 +++-- falcon/response_helpers.py | 8 ++++ falcon/testing/helpers.py | 2 +- falcon/util/__init__.py | 2 +- falcon/util/misc.py | 2 +- 12 files changed, 83 insertions(+), 69 deletions(-) diff --git a/falcon/_typing.py b/falcon/_typing.py index 7c6b0b498..4e6e12dc7 100644 --- a/falcon/_typing.py +++ b/falcon/_typing.py @@ -104,6 +104,9 @@ async def __call__( HeaderMapping = Mapping[str, str] HeaderIter = Iterable[Tuple[str, str]] HeaderArg = Union[HeaderMapping, HeaderIter] + +NarrowHeaderArg = Union[Dict[str,str], List[Tuple[str, str]]] + ResponseStatus = Union[http.HTTPStatus, str, int] StoreArg = Optional[Dict[str, Any]] Resource = object @@ -192,7 +195,7 @@ def __call__( # Media class SerializeSync(Protocol): - def __call__(self, media: Any, content_type: Optional[str] = ...) -> bytes: ... + def __call__(self, media: object, content_type: Optional[str] = ...) -> bytes: ... DeserializeSync = Callable[[bytes], Any] diff --git a/falcon/app.py b/falcon/app.py index f74083693..59253014a 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -1237,6 +1237,13 @@ def _get_body( # NOTE(kgriffs): Heuristic to quickly check if stream is # file-like. Not perfect, but should be good enough until # proven otherwise. + # + # TODO(jkmnt): The checks like these are a perfect candidates for the Python 3.13 TypeIs guard. + # The TypeGuard of Python 3.10+ seems to fit too, though it narrows type only for the 'if' branch. + # Something like: + # def is_readable_io(stream) -> TypeIs[ReadableStream]: + # return hasattr(stream, 'read') + # if hasattr(stream, 'read'): if wsgi_file_wrapper is not None: # TODO(kgriffs): Make block size configurable at the @@ -1251,7 +1258,7 @@ def _get_body( self._STREAM_BLOCK_SIZE, ) else: - iterable = stream + iterable = cast(Iterable[bytes], stream) return iterable, None @@ -1259,9 +1266,9 @@ def _get_body( def _update_sink_and_static_routes(self) -> None: if self._sink_before_static_route: - self._sink_and_static_routes = tuple(self._sinks + self._static_routes) # type: ignore[operator] + self._sink_and_static_routes = (*self._sinks, *self._static_routes) else: - self._sink_and_static_routes = tuple(self._static_routes + self._sinks) # type: ignore[operator] + self._sink_and_static_routes = (*self._static_routes, *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 5b09fa893..3687256f9 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -386,13 +386,13 @@ def __next__(self) -> bytes: def close(self) -> None: close_maybe(self._stream) - -def close_maybe(stream: Any): +# TODO(jkmnt): Move these to some other module, they don't belong here +def close_maybe(stream: Any) -> None: close: Callable[[], None] | None = getattr(stream, 'close', None) if close: close() -async def async_close_maybe(stream: Any): +async def async_close_maybe(stream: Any) -> None: close: Callable[[], Awaitable[None]] | None = getattr(stream, 'close', None) if close: await close() \ No newline at end of file diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index 4dade057f..ef95218c0 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -773,10 +773,11 @@ async def watch_disconnect() -> None: # (c) async iterator # - if hasattr(stream, 'read'): + read_meth: Callable[[int], Awaitable[bytes]] | None = getattr(stream, 'read') + if read_meth: try: while True: - data = await stream.read(self._STREAM_BLOCK_SIZE) + data = await read_meth(self._STREAM_BLOCK_SIZE) if data == b'': break else: @@ -1006,6 +1007,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 exception_tuple: Tuple[type[BaseException], ...] diff --git a/falcon/asgi/ws.py b/falcon/asgi/ws.py index 372a3f5b8..988c71d3d 100644 --- a/falcon/asgi/ws.py +++ b/falcon/asgi/ws.py @@ -19,6 +19,7 @@ from falcon.asgi_spec import WSCloseCode from falcon.constants import WebSocketPayloadType from falcon.util import misc +from falcon.response_helpers import _headers_to_items __all__ = ('WebSocket',) @@ -210,15 +211,9 @@ async def accept( 'does not support accept headers.' ) - header_items = getattr(headers, 'items', None) - if callable(header_items): - headers_iterable: Iterable[tuple[str, str]] = header_items() - else: - headers_iterable = headers # type: ignore[assignment] - event['headers'] = parsed_headers = [ (name.lower().encode('ascii'), value.encode('ascii')) - for name, value in headers_iterable + for name, value in _headers_to_items(headers) ] for name, __ in parsed_headers: diff --git a/falcon/errors.py b/falcon/errors.py index f47e82b07..d31450bd4 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 HeaderArg + from falcon._typing import NarrowHeaderArg from falcon.typing import Headers @@ -270,7 +270,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -350,7 +350,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, challenges: Optional[Iterable[str]] = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -425,7 +425,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -493,7 +493,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -614,7 +614,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) @@ -682,7 +682,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -752,7 +752,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -828,7 +828,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -889,7 +889,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -951,7 +951,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = 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[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -1104,7 +1104,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1166,7 +1166,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1242,7 +1242,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): headers = _load_headers(headers) @@ -1309,7 +1309,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1368,7 +1368,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1426,7 +1426,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1492,7 +1492,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1563,7 +1563,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, retry_after: RetryAfter = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -1629,7 +1629,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1701,7 +1701,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1759,7 +1759,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1824,7 +1824,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1882,7 +1882,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -1956,7 +1956,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, retry_after: RetryAfter = None, **kwargs: HTTPErrorKeywordArguments, ): @@ -2016,7 +2016,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2080,7 +2080,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2142,7 +2142,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2201,7 +2201,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2272,7 +2272,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): super().__init__( @@ -2328,7 +2328,7 @@ def __init__( msg: str, header_name: str, *, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = 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[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ): description = 'The "{0}" header is required.' @@ -2444,7 +2444,7 @@ def __init__( msg: str, param_name: str, *, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is invalid. {1}' @@ -2502,7 +2502,7 @@ def __init__( self, param_name: str, *, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: description = 'The "{0}" parameter is required.' @@ -2601,7 +2601,7 @@ class MediaMalformedError(HTTPBadRequest): """ def __init__( - self, media_type: str, **kwargs: Union[HeaderArg, HTTPErrorKeywordArguments] + self, media_type: str, **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments] ): super().__init__( title='Invalid {0}'.format(media_type), @@ -2672,7 +2672,7 @@ def __init__( *, title: Optional[str] = None, description: Optional[str] = None, - headers: Optional[HeaderArg] = None, + headers: Optional[NarrowHeaderArg] = None, **kwargs: HTTPErrorKeywordArguments, ) -> None: super().__init__( @@ -2701,13 +2701,13 @@ class MultipartParseError(MediaMalformedError): """ # NOTE(caselit): remove the description @property in MediaMalformedError - description = None + description = None # pyright: ignore[reportAssignmentType, reportGeneralTypeIssues] def __init__( self, *, description: Optional[str] = None, - **kwargs: Union[HeaderArg, HTTPErrorKeywordArguments], + **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments], ) -> None: HTTPBadRequest.__init__( self, @@ -2722,7 +2722,7 @@ def __init__( # ----------------------------------------------------------------------------- -def _load_headers(headers: Optional[HeaderArg]) -> Headers: +def _load_headers(headers: Optional[NarrowHeaderArg]) -> Headers: """Transform the headers to dict.""" if headers is None: return {} @@ -2732,9 +2732,9 @@ def _load_headers(headers: Optional[HeaderArg]) -> Headers: def _parse_retry_after( - headers: Optional[HeaderArg], + headers: Optional[NarrowHeaderArg], retry_after: RetryAfter, -) -> Optional[HeaderArg]: +) -> Optional[NarrowHeaderArg]: """Set the Retry-After to the headers when required.""" if retry_after is None: return headers diff --git a/falcon/media/base.py b/falcon/media/base.py index 0d80611f3..daa8e1040 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: str) -> bytes: + def serialize(self, media: object, content_type: Optional[str] = None) -> 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: str) -> bytes: Returns: bytes: The resulting serialized bytes from the input object. """ - if MEDIA_JSON in content_type: + if content_type is not None and 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/response.py b/falcon/response.py index 209f6f0d0..b0ce42010 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -22,6 +22,7 @@ import mimetypes from typing import ( Any, + Callable, ClassVar, Dict, Iterable, @@ -29,6 +30,7 @@ Mapping, NoReturn, Optional, + cast, overload, Tuple, Type, @@ -39,6 +41,7 @@ from falcon._typing import _UNSET from falcon._typing import RangeSetHeader from falcon._typing import UnsetOr +from falcon._typing import HeaderIter from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES from falcon.constants import DEFAULT_MEDIA_TYPE from falcon.errors import HeaderNotSupported @@ -48,6 +51,7 @@ 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 _headers_to_items from falcon.response_helpers import _is_ascii_encodable from falcon.typing import Headers from falcon.typing import ReadableIO @@ -826,16 +830,11 @@ def set_headers( or ``Iterable[[str, str]]``. """ - header_items = getattr(headers, 'items', None) - - if callable(header_items): - headers = header_items() - # NOTE(kgriffs): We can't use dict.update because we have to # normalize the header names. _headers = self._headers - for name, value in headers: # type: ignore[misc] + for name, value in _headers_to_items(headers): # NOTE(kgriffs): uwsgi fails with a TypeError if any header # is not a str, so do the conversion here. It's actually # faster to not do an isinstance check. str() will encode diff --git a/falcon/response_helpers.py b/falcon/response_helpers.py index 71d397ec5..87f481e59 100644 --- a/falcon/response_helpers.py +++ b/falcon/response_helpers.py @@ -19,6 +19,8 @@ from typing import Any, Callable, Iterable, Optional, TYPE_CHECKING from falcon._typing import RangeSetHeader +from falcon._typing import HeaderArg +from falcon._typing import HeaderIter from falcon.util import uri from falcon.util.misc import secure_filename @@ -145,3 +147,9 @@ def _is_ascii_encodable(s: str) -> bool: # NOTE(tbug): s is probably not a string type return False return True + +def _headers_to_items(headers: HeaderArg) -> HeaderIter: + header_items: Callable[[], HeaderIter] | None = getattr(headers, 'items', None) + if callable(header_items): + return header_items() + return headers # type: ignore[return-value] \ No newline at end of file diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index c8d192f8f..df23e3391 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -169,7 +169,7 @@ def __init__( if chunk_size is None: chunk_size = 4096 - self._body = memoryview(body) + self._body: Optional[memoryview] = memoryview(body) self._chunk_size = chunk_size self._emit_empty_chunks = True self._disconnect_at = disconnect_at diff --git a/falcon/util/__init__.py b/falcon/util/__init__.py index a52a197cd..129d6c3b5 100644 --- a/falcon/util/__init__.py +++ b/falcon/util/__init__.py @@ -70,7 +70,7 @@ from falcon.util.reader import BufferedReader as _PyBufferedReader # NOQA try: - from falcon.cyutil.reader import BufferedReader as _CyBufferedReader + from falcon.cyutil.reader import BufferedReader as _CyBufferedReader # pyright: ignore[reportMissingImports] except ImportError: _CyBufferedReader = None diff --git a/falcon/util/misc.py b/falcon/util/misc.py index ae17c12a1..5dd58a8f8 100644 --- a/falcon/util/misc.py +++ b/falcon/util/misc.py @@ -42,7 +42,7 @@ from .deprecation import deprecated try: - from falcon.cyutil.misc import encode_items_to_latin1 as _cy_encode_items_to_latin1 + from falcon.cyutil.misc import encode_items_to_latin1 as _cy_encode_items_to_latin1 # pyright: ignore[reportMissingImports] except ImportError: _cy_encode_items_to_latin1 = None From 7483edc543cdf52b3bccfbde38c7b930bb0ba106 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Wed, 16 Oct 2024 01:40:17 +0300 Subject: [PATCH 05/10] small fix after running tests --- falcon/asgi/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index ef95218c0..2a8397df4 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -773,7 +773,7 @@ async def watch_disconnect() -> None: # (c) async iterator # - read_meth: Callable[[int], Awaitable[bytes]] | None = getattr(stream, 'read') + read_meth: Callable[[int], Awaitable[bytes]] | None = getattr(stream, 'read', None) if read_meth: try: while True: From 8db1d96ddb4bac02baf524a55220843548caf5bb Mon Sep 17 00:00:00 2001 From: jkmnt Date: Wed, 16 Oct 2024 01:45:40 +0300 Subject: [PATCH 06/10] few more changes --- falcon/media/msgpack.py | 2 +- falcon/media/multipart.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/falcon/media/msgpack.py b/falcon/media/msgpack.py index 5b8c587c9..95931811d 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]) -> bytes: + def serialize(self, media: Any, content_type: Optional[str] = None) -> 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 99cd8785e..cf1aba289 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: str) -> NoReturn: + def serialize(self, media: object, content_type: Optional[str] = None) -> NoReturn: raise NotImplementedError('multipart form serialization unsupported') From ce15bfcc1ada33de306d39981c7198bf86792712 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Wed, 16 Oct 2024 13:30:03 +0300 Subject: [PATCH 07/10] Changed iterable of tuples in NarrowHeaderArg to sequence of tuples. Ruff run. --- falcon/__init__.py | 10 +++++----- falcon/_typing.py | 3 ++- falcon/app.py | 6 +++--- falcon/app_helpers.py | 21 +++++++++++++++++---- falcon/asgi/app.py | 24 ++++++++++++++---------- falcon/asgi/ws.py | 4 ++-- falcon/errors.py | 6 ++++-- falcon/inspect.py | 5 ++++- falcon/response.py | 3 --- falcon/response_helpers.py | 7 ++++--- falcon/routing/compiled.py | 2 +- falcon/testing/helpers.py | 2 +- falcon/util/__init__.py | 4 +++- falcon/util/misc.py | 4 +++- 14 files changed, 63 insertions(+), 38 deletions(-) diff --git a/falcon/__init__.py b/falcon/__init__.py index 67189d73d..cdc19f4dc 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -407,6 +407,11 @@ 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 @@ -637,10 +642,5 @@ from falcon.util import wrap_sync_to_async from falcon.util import wrap_sync_to_async_unsafe -# 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, logging as _logging - # Package version from falcon.version import __version__ # NOQA: F401 - diff --git a/falcon/_typing.py b/falcon/_typing.py index 4e6e12dc7..946bf2c02 100644 --- a/falcon/_typing.py +++ b/falcon/_typing.py @@ -32,6 +32,7 @@ Optional, Pattern, Protocol, + Sequence, Tuple, TYPE_CHECKING, TypeVar, @@ -105,7 +106,7 @@ async def __call__( HeaderIter = Iterable[Tuple[str, str]] HeaderArg = Union[HeaderMapping, HeaderIter] -NarrowHeaderArg = Union[Dict[str,str], List[Tuple[str, str]]] +NarrowHeaderArg = Union[Mapping[str, str], Sequence[Tuple[str, str]]] ResponseStatus = Union[http.HTTPStatus, str, int] StoreArg = Optional[Dict[str, Any]] diff --git a/falcon/app.py b/falcon/app.py index 59253014a..d48243a11 100644 --- a/falcon/app.py +++ b/falcon/app.py @@ -1237,9 +1237,9 @@ def _get_body( # NOTE(kgriffs): Heuristic to quickly check if stream is # file-like. Not perfect, but should be good enough until # proven otherwise. - # - # TODO(jkmnt): The checks like these are a perfect candidates for the Python 3.13 TypeIs guard. - # The TypeGuard of Python 3.10+ seems to fit too, though it narrows type only for the 'if' branch. + # TODO(jkmnt): The checks like these are a perfect candidates for the + # Python 3.13 TypeIs guard. The TypeGuard of Python 3.10+ seems to fit too, + # though it narrows type only for the 'if' branch. # Something like: # def is_readable_io(stream) -> TypeIs[ReadableStream]: # return hasattr(stream, 'read') diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 3687256f9..5972054ed 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -17,7 +17,18 @@ from __future__ import annotations from inspect import iscoroutinefunction -from typing import Any, Awaitable, Callable, Iterable, List, Literal, Optional, overload, Tuple, Union +from typing import ( + Any, + Awaitable, + Callable, + Iterable, + List, + Literal, + Optional, + overload, + Tuple, + Union, +) from falcon import util from falcon._typing import AsgiProcessRequestMethod as APRequest @@ -386,13 +397,15 @@ def __next__(self) -> bytes: 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: Callable[[], None] | None = getattr(stream, 'close', None) + close: Optional[Callable[[], None]] = getattr(stream, 'close', None) if close: close() + async def async_close_maybe(stream: Any) -> None: - close: Callable[[], Awaitable[None]] | None = getattr(stream, 'close', None) + close: Optional[Callable[[], Awaitable[None]]] = getattr(stream, 'close', None) if close: - await close() \ No newline at end of file + await close() diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index 2a8397df4..0613032de 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -42,8 +42,8 @@ from falcon import routing from falcon._typing import _UNSET from falcon._typing import AsgiErrorHandler -from falcon._typing import AsgiProcessStartupMethod 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,28 +51,28 @@ 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 from falcon.app_helpers import prepare_middleware_ws -from falcon.app_helpers import async_close_maybe from falcon.asgi_spec import AsgiSendMsg from falcon.asgi_spec import EventType from falcon.asgi_spec import WSCloseCode from falcon.constants import MEDIA_JSON from falcon.errors import CompatibilityError from falcon.errors import HTTPBadRequest -from falcon.errors import WebSocketDisconnected from falcon.errors import HTTPInternalServerError +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 from falcon.util.sync import _should_wrap_non_coroutines from falcon.util.sync import _wrap_non_coroutine_unsafe from falcon.util.sync import wrap_sync_to_async -from falcon.logger import _logger from ._asgi_helpers import _validate_asgi_scope from ._asgi_helpers import _wrap_asgi_coroutine_func @@ -773,7 +773,9 @@ async def watch_disconnect() -> None: # (c) async iterator # - read_meth: Callable[[int], Awaitable[bytes]] | None = getattr(stream, 'read', None) + read_meth: Optional[Callable[[int], Awaitable[bytes]]] = getattr( + stream, 'read', None + ) if read_meth: try: while True: @@ -1080,7 +1082,9 @@ async def _call_lifespan_handlers( return for handler in self._unprepared_middleware: - process_startup: AsgiProcessStartupMethod | None = getattr(handler, 'process_startup', None) + process_startup: Optional[AsgiProcessStartupMethod] = getattr( + handler, 'process_startup', None + ) if process_startup: try: await process_startup(scope, event) @@ -1097,7 +1101,9 @@ async def _call_lifespan_handlers( elif event['type'] == 'lifespan.shutdown': for handler in reversed(self._unprepared_middleware): - process_shutdown = getattr(handler, 'process_shutdown', None) + process_shutdown: Optional[AsgiProcessShutdownMethod] = getattr( + handler, 'process_shutdown', None + ) if process_shutdown: try: await process_shutdown(scope, event) @@ -1251,9 +1257,7 @@ async def _ws_disconnected_error_handler( ) -> None: assert resp is None assert ws is not None - _logger.debug( - '[FALCON] WebSocket client disconnected with code %i', error.code - ) + _logger.debug('[FALCON] WebSocket client disconnected with code %i', error.code) await self._ws_cleanup_on_error(ws) if TYPE_CHECKING: diff --git a/falcon/asgi/ws.py b/falcon/asgi/ws.py index 988c71d3d..6f8ba7191 100644 --- a/falcon/asgi/ws.py +++ b/falcon/asgi/ws.py @@ -5,7 +5,7 @@ from enum import auto from enum import Enum import re -from typing import Any, Deque, Dict, Iterable, Mapping, Optional, Tuple, Union +from typing import Any, Deque, Dict, Mapping, Optional, Tuple, Union from falcon import errors from falcon import media @@ -18,8 +18,8 @@ from falcon.asgi_spec import EventType from falcon.asgi_spec import WSCloseCode from falcon.constants import WebSocketPayloadType -from falcon.util import misc from falcon.response_helpers import _headers_to_items +from falcon.util import misc __all__ = ('WebSocket',) diff --git a/falcon/errors.py b/falcon/errors.py index d31450bd4..6c647ba05 100644 --- a/falcon/errors.py +++ b/falcon/errors.py @@ -2601,7 +2601,9 @@ class MediaMalformedError(HTTPBadRequest): """ def __init__( - self, media_type: str, **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments] + self, + media_type: str, + **kwargs: Union[NarrowHeaderArg, HTTPErrorKeywordArguments], ): super().__init__( title='Invalid {0}'.format(media_type), @@ -2701,7 +2703,7 @@ class MultipartParseError(MediaMalformedError): """ # NOTE(caselit): remove the description @property in MediaMalformedError - description = None # pyright: ignore[reportAssignmentType, reportGeneralTypeIssues] + description = None # pyright: ignore[reportAssignmentType, reportGeneralTypeIssues] def __init__( self, diff --git a/falcon/inspect.py b/falcon/inspect.py index 06dbd3185..7cb749d0d 100644 --- a/falcon/inspect.py +++ b/falcon/inspect.py @@ -248,7 +248,10 @@ def _traverse(roots: List[CompiledRouterNode], parent: str) -> None: 'info will always be a string' ) method_info = RouteMethodInfo( - method, source_info, getattr(real_func, '__name__', '?'), internal + method, + source_info, + getattr(real_func, '__name__', '?'), + internal, ) methods.append(method_info) source_info, class_name = _get_source_info_and_name(root.resource) diff --git a/falcon/response.py b/falcon/response.py index b0ce42010..cbf92295a 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -22,7 +22,6 @@ import mimetypes from typing import ( Any, - Callable, ClassVar, Dict, Iterable, @@ -30,7 +29,6 @@ Mapping, NoReturn, Optional, - cast, overload, Tuple, Type, @@ -41,7 +39,6 @@ from falcon._typing import _UNSET from falcon._typing import RangeSetHeader from falcon._typing import UnsetOr -from falcon._typing import HeaderIter from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES from falcon.constants import DEFAULT_MEDIA_TYPE from falcon.errors import HeaderNotSupported diff --git a/falcon/response_helpers.py b/falcon/response_helpers.py index 87f481e59..1564fcc71 100644 --- a/falcon/response_helpers.py +++ b/falcon/response_helpers.py @@ -18,9 +18,9 @@ from typing import Any, Callable, Iterable, Optional, TYPE_CHECKING -from falcon._typing import RangeSetHeader from falcon._typing import HeaderArg from falcon._typing import HeaderIter +from falcon._typing import RangeSetHeader from falcon.util import uri from falcon.util.misc import secure_filename @@ -148,8 +148,9 @@ def _is_ascii_encodable(s: str) -> bool: return False return True + def _headers_to_items(headers: HeaderArg) -> HeaderIter: - header_items: Callable[[], HeaderIter] | None = getattr(headers, 'items', None) + header_items: Optional[Callable[[], HeaderIter]] = getattr(headers, 'items', None) if callable(header_items): return header_items() - return headers # type: ignore[return-value] \ No newline at end of file + return headers # type: ignore[return-value] diff --git a/falcon/routing/compiled.py b/falcon/routing/compiled.py index 961233d81..06d19576b 100644 --- a/falcon/routing/compiled.py +++ b/falcon/routing/compiled.py @@ -435,7 +435,7 @@ def _generate_ast( # noqa: C901 nodes: List[CompiledRouterNode], parent: _CxParent, return_values: List[CompiledRouterNode], - patterns: List[Pattern], + patterns: List[Pattern[str]], params_stack: List[_CxElement], level: int = 0, fast_return: bool = True, diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index df23e3391..3b0ef50fe 100644 --- a/falcon/testing/helpers.py +++ b/falcon/testing/helpers.py @@ -59,6 +59,7 @@ 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 @@ -69,7 +70,6 @@ from falcon.util import code_to_http_status from falcon.util import uri from falcon.util.mediatypes import parse_header -from falcon.app_helpers import close_maybe # NOTE(kgriffs): Changed in 3.0 from 'curl/7.24.0 (x86_64-apple-darwin12.0)' DEFAULT_UA = 'falcon-client/' + falcon.__version__ diff --git a/falcon/util/__init__.py b/falcon/util/__init__.py index 129d6c3b5..b6852e36b 100644 --- a/falcon/util/__init__.py +++ b/falcon/util/__init__.py @@ -70,7 +70,9 @@ from falcon.util.reader import BufferedReader as _PyBufferedReader # NOQA try: - from falcon.cyutil.reader import BufferedReader as _CyBufferedReader # pyright: ignore[reportMissingImports] + from falcon.cyutil.reader import ( # pyright: ignore[reportMissingImports] + BufferedReader as _CyBufferedReader, + ) except ImportError: _CyBufferedReader = None diff --git a/falcon/util/misc.py b/falcon/util/misc.py index 5dd58a8f8..c42dbb2b4 100644 --- a/falcon/util/misc.py +++ b/falcon/util/misc.py @@ -42,7 +42,9 @@ from .deprecation import deprecated try: - from falcon.cyutil.misc import encode_items_to_latin1 as _cy_encode_items_to_latin1 # pyright: ignore[reportMissingImports] + from falcon.cyutil.misc import ( # pyright: ignore[reportMissingImports] + encode_items_to_latin1 as _cy_encode_items_to_latin1, + ) except ImportError: _cy_encode_items_to_latin1 = None From 1deda2ff6aa4ff143b17e1cb943c668b3d4f2f03 Mon Sep 17 00:00:00 2001 From: jkmnt Date: Wed, 16 Oct 2024 13:39:09 +0300 Subject: [PATCH 08/10] cosmetic --- falcon/response.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/falcon/response.py b/falcon/response.py index cbf92295a..70db069d1 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -26,7 +26,6 @@ Dict, Iterable, List, - Mapping, NoReturn, Optional, overload, @@ -37,6 +36,7 @@ ) from falcon._typing import _UNSET +from falcon._typing import HeaderArg from falcon._typing import RangeSetHeader from falcon._typing import UnsetOr from falcon.constants import _DEFAULT_STATIC_MEDIA_TYPES @@ -793,9 +793,7 @@ def append_header(self, name: str, value: str) -> None: self._headers[name] = value - def set_headers( - self, headers: Union[Mapping[str, str], Iterable[Tuple[str, str]]] - ) -> None: + def set_headers(self, headers: HeaderArg) -> None: """Set several headers at once. This method can be used to set a collection of raw header names and From 729cc112448da67acecd6ef11c7d11035a657d1e Mon Sep 17 00:00:00 2001 From: jkmnt Date: Fri, 18 Oct 2024 15:29:44 +0300 Subject: [PATCH 09/10] reverted the internal logger back to the main __init__.py --- falcon/__init__.py | 12 +++++++----- falcon/asgi/app.py | 2 +- 2 files changed, 8 insertions(+), 6 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/asgi/app.py b/falcon/asgi/app.py index 0613032de..81d774a62 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -37,6 +37,7 @@ Union, ) +from falcon import _logger from falcon import constants from falcon import responders from falcon import routing @@ -66,7 +67,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 From 8465649f89abb120ed82d2da4715ac3bca49c1bf Mon Sep 17 00:00:00 2001 From: jkmnt Date: Fri, 18 Oct 2024 15:39:18 +0300 Subject: [PATCH 10/10] inlined stream.close calls to better match falcon style --- falcon/app_helpers.py | 19 +++---------------- falcon/asgi/app.py | 19 ++++++++++--------- falcon/testing/helpers.py | 5 +++-- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/falcon/app_helpers.py b/falcon/app_helpers.py index 5972054ed..7e13fe054 100644 --- a/falcon/app_helpers.py +++ b/falcon/app_helpers.py @@ -18,8 +18,6 @@ from inspect import iscoroutinefunction from typing import ( - Any, - Awaitable, Callable, Iterable, List, @@ -395,17 +393,6 @@ 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() + close: Optional[Callable[[], None]] = getattr(self._stream, 'close', None) + if close: + close() diff --git a/falcon/asgi/app.py b/falcon/asgi/app.py index 81d774a62..64ce50b8f 100644 --- a/falcon/asgi/app.py +++ b/falcon/asgi/app.py @@ -52,7 +52,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 @@ -773,13 +772,16 @@ async def watch_disconnect() -> None: # (c) async iterator # - read_meth: Optional[Callable[[int], Awaitable[bytes]]] = getattr( + read: Optional[Callable[[int], Awaitable[bytes]]] = getattr( stream, 'read', None ) - if read_meth: + close: Optional[Callable[[], Awaitable[None]]] = getattr( + stream, 'close', None + ) + if read: try: while True: - data = await read_meth(self._STREAM_BLOCK_SIZE) + data = await read(self._STREAM_BLOCK_SIZE) if data == b'': break else: @@ -793,7 +795,8 @@ async def watch_disconnect() -> None: } ) finally: - await async_close_maybe(stream) + if close: + await close() else: # NOTE(kgriffs): Works for both async generators and iterators try: @@ -829,10 +832,8 @@ async def watch_disconnect() -> None: 'Response.stream: ' + str(ex) ) finally: - # 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 close: + await close() await send(_EVT_RESP_EOF) diff --git a/falcon/testing/helpers.py b/falcon/testing/helpers.py index 3b0ef50fe..103ea414d 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,9 @@ def wrapper() -> Iterator[bytes]: for item in iterable: yield item finally: - close_maybe(iterable) + close: Optional[Callable[[], None]] = getattr(iterable, 'close', None) + if close: + close() wrapped = wrapper() head: Tuple[bytes, ...]