Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_newsfragments/2629.newandimproved.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Internal LRU cache decorators now preserve wrapped callable signatures for
static type checking.
3 changes: 1 addition & 2 deletions falcon/http_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ def status_code(self) -> int:
"""HTTP status code normalized from the ``status`` argument passed
to the initializer.
""" # noqa: D205
# TODO(0xMattB): Modify decorator to return proper type (see gh #2629).
return misc.http_status_to_code(self.status) # type: ignore[no-any-return]
return misc.http_status_to_code(self.status)

def to_dict(
self, obj_type: type[MutableMapping[str, str | int | None | Link]] = dict
Expand Down
3 changes: 1 addition & 2 deletions falcon/http_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ def __init__(
@property
def status_code(self) -> int:
"""HTTP status code normalized from :attr:`status`."""
# TODO(0xMattB): Modify decorator to return proper type (see PR #2629).
return http_status_to_code(self.status) # type: ignore[no-any-return]
return http_status_to_code(self.status)
6 changes: 4 additions & 2 deletions falcon/media/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def _raise(self, *args: Any, **kwargs: Any) -> NoReturn:


class ResolverMethod(Protocol):
def cache_clear(self) -> None: ...

@overload
def __call__(
self, media_type: str | None, default: str, raise_not_found: Literal[False]
Expand Down Expand Up @@ -98,14 +100,14 @@ def __setitem__(self, key: str, value: BaseHandler) -> None:
# NOTE(kgriffs): When the mapping changes, we do not want to use a
# cached handler from the previous mapping, in case it was
# replaced.
self._resolve.cache_clear() # type: ignore[attr-defined]
self._resolve.cache_clear()

def __delitem__(self, key: str) -> None:
super().__delitem__(key)

# NOTE(kgriffs): Similar to __setitem__(), we need to avoid resolving
# to a cached handler that was removed.
self._resolve.cache_clear() # type: ignore[attr-defined]
self._resolve.cache_clear()

def _create_resolver(self) -> ResolverMethod:
# PERF(kgriffs): Under PyPy the LRU is relatively expensive as compared
Expand Down
3 changes: 1 addition & 2 deletions falcon/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ def status_code(self) -> int:
if resp.status_code >= 400:
log.warning(f'returning error response: {resp.status_code}')
"""
# TODO(0xMattB): Modify decorator to return proper type (see gh #2629).
return http_status_to_code(self.status) # type: ignore[no-any-return]
return http_status_to_code(self.status)

@status_code.setter
def status_code(self, value: int) -> None:
Expand Down
8 changes: 6 additions & 2 deletions falcon/testing/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,19 +953,23 @@ async def _simulate_request_asgi(
while not resp_event_collector.status:
await asyncio.sleep(0)

status = resp_event_collector.status
assert status is not None
return StreamedResult(
resp_event_collector.body_chunks,
code_to_http_status(resp_event_collector.status),
code_to_http_status(status),
resp_event_collector.headers,
task_req,
req_event_emitter,
)

req_event_emitter.disconnect()
await task_req
final_status = resp_event_collector.status
assert final_status is not None
return Result(
resp_event_collector.body_chunks,
code_to_http_status(resp_event_collector.status),
code_to_http_status(final_status),
resp_event_collector.headers,
)

Expand Down
33 changes: 27 additions & 6 deletions falcon/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import os
import os.path
import re
from typing import Any, Callable
from typing import Any, Callable, cast, TYPE_CHECKING
import unicodedata

from falcon import status_codes
Expand All @@ -44,6 +44,23 @@
# public Falcon interface.
from .deprecation import deprecated

if TYPE_CHECKING:
from typing import ParamSpec, Protocol, TypeVar

_P = ParamSpec('_P')
_R_co = TypeVar('_R_co', covariant=True)

class _CallableWithCacheClear(Protocol[_P, _R_co]):
cache_clear: Callable[[], None]

def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...

class _LruCacheFactory(Protocol):
def __call__(
self, maxsize: int
) -> Callable[[Callable[_P, _R_co]], _CallableWithCacheClear[_P, _R_co]]: ...


try:
from falcon.cyutil.misc import encode_items_to_latin1 as _cy_encode_items_to_latin1
except ImportError:
Expand Down Expand Up @@ -103,23 +120,27 @@
# the nocover pragma here.
def _lru_cache_nop(
maxsize: int,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]: # pragma: nocover
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
) -> Callable[
[Callable[_P, _R_co]], _CallableWithCacheClear[_P, _R_co]
]: # pragma: nocover
def decorator(func: Callable[_P, _R_co]) -> _CallableWithCacheClear[_P, _R_co]:
# NOTE(kgriffs): Partially emulate the lru_cache protocol; only add
# cache_info() later if/when it becomes necessary.
func.cache_clear = lambda: None # type: ignore
cached_func = cast('_CallableWithCacheClear[_P, _R_co]', func)
cached_func.cache_clear = lambda: None

return func
return cached_func

return decorator


# PERF(kgriffs): Using lru_cache is slower on PyPy when the wrapped
# function is just doing a few non-IO operations.
_lru_cache_for_simple_logic: _LruCacheFactory
if PYPY:
_lru_cache_for_simple_logic = _lru_cache_nop # pragma: nocover
else:
_lru_cache_for_simple_logic = functools.lru_cache
_lru_cache_for_simple_logic = cast('_LruCacheFactory', functools.lru_cache)


def is_python_func(func: Callable[..., Any] | Any) -> bool:
Expand Down
Loading