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
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 @@ -952,19 +952,23 @@ async def _simulate_request_asgi(
while not resp_event_collector.status:
await asyncio.sleep(0)

status = resp_event_collector.status

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please undo these unrelated changes?
Or does going via a variable somehow help with typing?

Since this is the testing client's code, we are not that concerned about performance, but it would still be good to understand why exactly we need this pattern.

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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These protocols look good at first glance. 👍

But could you please move them to falcon/_typing.py? We are trying to colocate such definitions in that module.

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