Skip to content

Commit

Permalink
Add boundary types for before hooks
Browse files Browse the repository at this point in the history
** BREAKING CHANGE **

This adds typing information for the before hook as a proof of concept. I used
ParamSpec as a means to introspect the hooks arguments to type check the *args
and **kwargs arguments.

A problem with using ParamSpec is that additional arguments to the before
decorator become impossible (so far as I can see) to specify a type. is_async,
in needs to come after the positional args, but that is not allowed with
ParamSpec. The solution to this is to move is_async to be consumed through
**kwargs and no longer a special keyword argument. I don't think this is actually
a breaking change, but because it changes a signature it might be worth marking
it as such.
  • Loading branch information
nZac committed Jun 22, 2024
1 parent b1c6fb2 commit d0b2358
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 57 deletions.
106 changes: 53 additions & 53 deletions falcon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,57 +101,57 @@


__all__ = (
"API",
"App",
"after",
"before",
"HTTPError",
"HTTPStatus",
"CORSMiddleware",
"HTTPFound",
"HTTPMovedPermanently",
"HTTPPermanentRedirect",
"HTTPSeeOther",
"HTTPTemporaryRedirect",
"Forwarded",
"Request",
"RequestOptions",
"Response",
"ResponseOptions",
"BoundedStream",
"async_to_sync",
"BufferedReader",
"CaseInsensitiveDict",
"code_to_http_status",
"Context",
"create_task",
"deprecated",
"dt_to_http",
"ETag",
"get_argnames",
"get_bound_method",
"get_http_status",
"get_running_loop",
"http_cookies",
"http_date_to_dt",
"http_now",
"http_status_to_code",
"IS_64_BITS",
"is_python_func",
"misc",
"parse_header",
"reader",
"runs_sync",
"secure_filename",
"structures",
"sync",
"sync_to_async",
"sys",
"time",
"TimezoneGMT",
"to_query_str",
"uri",
"wrap_sync_to_async",
"wrap_sync_to_async_unsafe",
"__version__",
'API',
'App',
'after',
'before',
'HTTPError',
'HTTPStatus',
'CORSMiddleware',
'HTTPFound',
'HTTPMovedPermanently',
'HTTPPermanentRedirect',
'HTTPSeeOther',
'HTTPTemporaryRedirect',
'Forwarded',
'Request',
'RequestOptions',
'Response',
'ResponseOptions',
'BoundedStream',
'async_to_sync',
'BufferedReader',
'CaseInsensitiveDict',
'code_to_http_status',
'Context',
'create_task',
'deprecated',
'dt_to_http',
'ETag',
'get_argnames',
'get_bound_method',
'get_http_status',
'get_running_loop',
'http_cookies',
'http_date_to_dt',
'http_now',
'http_status_to_code',
'IS_64_BITS',
'is_python_func',
'misc',
'parse_header',
'reader',
'runs_sync',
'secure_filename',
'structures',
'sync',
'sync_to_async',
'sys',
'time',
'TimezoneGMT',
'to_query_str',
'uri',
'wrap_sync_to_async',
'wrap_sync_to_async_unsafe',
'__version__',
)
21 changes: 17 additions & 4 deletions falcon/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,32 @@
"""Hook decorators."""

from functools import wraps
from inspect import getmembers
from inspect import iscoroutinefunction
from inspect import getmembers, iscoroutinefunction
import re

import typing_extensions as te

from falcon.constants import COMBINED_METHODS
from falcon.request import Request
from falcon.response import Response
from falcon.util.misc import get_argnames
from falcon.util.sync import _wrap_non_coroutine_unsafe


_DECORABLE_METHOD_NAME = re.compile(
r'^on_({})(_\w+)?$'.format('|'.join(method.lower() for method in COMBINED_METHODS))
)


def before(action, *args, is_async=False, **kwargs):
P = te.ParamSpec('P')
R = te.TypeVar('R', bound=Request)
S = te.TypeVar('S', bound=Response)


def before(
action: te.Callable[te.Concatenate[R, S, te.Any, te.Dict[str, te.Any], P], None],
*args: P.args,
**kwargs: P.kwargs,
) -> te.Callable[[te.Any], te.Any]:
"""Execute the given action function *before* the responder.
The `params` argument that is passed to the hook
Expand Down Expand Up @@ -79,6 +90,8 @@ def do_something(req, resp, resource, params):
*action*.
"""

is_async = kwargs.get('is_async', False)

def _before(responder_or_resource):
if isinstance(responder_or_resource, type):
resource = responder_or_resource
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"setuptools>=47",
"wheel>=0.34",
"cython>=0.29.21; python_implementation == 'CPython'", # Skip cython when using pypy
"typing-extensions>=4"
]

[tool.mypy]
Expand Down

0 comments on commit d0b2358

Please sign in to comment.