Skip to content

Commit 587bffd

Browse files
vytas7jap
andauthored
feat(typing): parametrize App by default req/resp types on 3.13+ (#2591)
* feat(typing): use TypeVar defaults on 3.13+ (WiP) * feat(typing): clean up _ReqT/_RespT with defaults * docs(typing): document the new improvements Co-authored-by: Jasper Spaans <j@jasper.es> --------- Co-authored-by: Jasper Spaans <j@jasper.es>
1 parent 6de14de commit 587bffd

File tree

7 files changed

+50
-28
lines changed

7 files changed

+50
-28
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:ref:`Generic App types <generic_app_types>` are now automatically parametrized
2+
by the default request/response types (unless specified otherwise), courtesy of
3+
:class:`~typing.TypeVar`\'s default value support on CPython 3.13+.

docs/api/typing.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ The use of generics should in most cases require no explicit effort on your
4444
side. However, if you annotate your variables or return types as
4545
``falcon.App``, the type checker may require you to provide the explicit type
4646
parameters when running in the strict mode (Mypy calls the option
47-
``--disallow-any-generics``, also part of the ``--strict`` mode flag).
47+
``--disallow-any-generics``, also part of the ``--strict`` mode flag) on
48+
Python 3.12 or older.
4849

4950
For instance, the following mini-application will not pass type checking with
50-
Mypy in the ``--strict`` mode:
51+
Mypy in the ``--strict`` mode on, e.g., CPython 3.12:
5152

5253
.. code-block:: python
5354
@@ -123,6 +124,11 @@ Both alternatives should now pass type checking in the ``--strict`` mode.
123124
:class:`generic types <typing.Generic>` parametrized by the request and
124125
response classes.
125126

127+
.. versionchanged:: 4.3
128+
:class:`falcon.App` and :class:`falcon.asgi.App` are now automatically
129+
parametrized by the default request/response types unless specified
130+
otherwise. However, this only works on CPython 3.13+.
131+
126132

127133
Known Limitations
128134
-----------------

docs/changes/4.3.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Many thanks to all of our talented and stylish contributors for this release!
2525
- `0x1618 <https://github.com/0x1618>`__
2626
- `0xMattB <https://github.com/0xMattB>`__
2727
- `granuels <https://github.com/granuels>`__
28+
- `jap <https://github.com/jap>`__
2829
- `rushevich <https://github.com/rushevich>`__
2930
- `thisisrick25 <https://github.com/thisisrick25>`__
3031
- `TudorGR <https://github.com/TudorGR>`__

falcon/_typing.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""Private type aliases used internally by Falcon.."""
14+
"""Private type aliases used internally by Falcon."""
1515

1616
from __future__ import annotations
1717

@@ -63,10 +63,23 @@ class _Unset(Enum):
6363
_UNSET = _Unset.UNSET
6464
UnsetOr = Union[Literal[_Unset.UNSET], _T]
6565

66-
_ReqT = TypeVar('_ReqT', bound='Request', contravariant=True)
67-
_RespT = TypeVar('_RespT', bound='Response', contravariant=True)
68-
_AReqT = TypeVar('_AReqT', bound='AsgiRequest', contravariant=True)
69-
_ARespT = TypeVar('_ARespT', bound='AsgiResponse', contravariant=True)
66+
# NOTE(vytas,jap): TypeVar's "default" argument is only available on 3.13+.
67+
if sys.version_info >= (3, 13):
68+
_ExcT = TypeVar('_ExcT', bound=Exception, default=Exception)
69+
_ReqT = TypeVar('_ReqT', bound='Request', contravariant=True, default='Request')
70+
_RespT = TypeVar('_RespT', bound='Response', contravariant=True, default='Response')
71+
_AReqT = TypeVar(
72+
'_AReqT', bound='AsgiRequest', contravariant=True, default='AsgiRequest'
73+
)
74+
_ARespT = TypeVar(
75+
'_ARespT', bound='AsgiResponse', contravariant=True, default='AsgiResponse'
76+
)
77+
else:
78+
_ExcT = TypeVar('_ExcT', bound=Exception)
79+
_ReqT = TypeVar('_ReqT', bound='Request', contravariant=True)
80+
_RespT = TypeVar('_RespT', bound='Response', contravariant=True)
81+
_AReqT = TypeVar('_AReqT', bound='AsgiRequest', contravariant=True)
82+
_ARespT = TypeVar('_ARespT', bound='AsgiResponse', contravariant=True)
7083

7184
Link = dict[str, str]
7285
CookieArg = Mapping[str, Union[str, Cookie]]

falcon/app.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@
3131
Generic,
3232
Literal,
3333
overload,
34-
TypeVar,
3534
)
3635
import warnings
3736

3837
from falcon import app_helpers as helpers
3938
from falcon import constants
4039
from falcon import responders
4140
from falcon import routing
41+
from falcon._typing import _ExcT
42+
from falcon._typing import _ReqT
43+
from falcon._typing import _RespT
4244
from falcon._typing import AsgiResponderCallable
4345
from falcon._typing import AsgiResponderWsCallable
44-
from falcon._typing import AsgiSinkCallable
4546
from falcon._typing import ErrorHandler
4647
from falcon._typing import ErrorSerializer
4748
from falcon._typing import FindMethod
@@ -86,10 +87,6 @@
8687
]
8788
)
8889

89-
_ExcT = TypeVar('_ExcT', bound=Exception)
90-
_ReqT = TypeVar('_ReqT', bound=Request, contravariant=True)
91-
_RespT = TypeVar('_RespT', bound=Response, contravariant=True)
92-
9390

9491
class App(Generic[_ReqT, _RespT]):
9592
'''The main entry point into a Falcon-based WSGI app.
@@ -268,17 +265,17 @@ def process_response(
268265
# NOTE(caselit): this should actually be a protocol of the methods required
269266
# by a router, hardcoded to CompiledRouter for convenience for now.
270267
_router: routing.CompiledRouter
271-
_serialize_error: ErrorSerializer
268+
_serialize_error: ErrorSerializer[_ReqT, _RespT]
272269
_sink_and_static_routes: tuple[
273270
tuple[
274271
Pattern[str] | routing.StaticRoute,
275-
SinkCallable | AsgiSinkCallable | routing.StaticRoute,
272+
SinkCallable[_ReqT, _RespT] | routing.StaticRoute,
276273
bool,
277274
],
278275
...,
279276
]
280277
_sink_before_static_route: bool
281-
_sinks: list[tuple[Pattern[str], SinkCallable | AsgiSinkCallable, Literal[True]]]
278+
_sinks: list[tuple[Pattern[str], SinkCallable[_ReqT, _RespT], Literal[True]]]
282279
_static_routes: list[
283280
tuple[routing.StaticRoute, routing.StaticRoute, Literal[False]]
284281
]
@@ -1167,7 +1164,7 @@ def _get_responder(
11671164
if m:
11681165
if is_sink:
11691166
params = m.groupdict() # type: ignore[union-attr]
1170-
responder = obj
1167+
responder = obj # type: ignore[assignment,unused-ignore]
11711168

11721169
break
11731170
else:
@@ -1220,7 +1217,7 @@ def _python_error_handler(
12201217
req.log_error(traceback.format_exc())
12211218
self._compose_error_response(req, resp, HTTPInternalServerError())
12221219

1223-
def _find_error_handler(self, ex: Exception) -> ErrorHandler | None:
1220+
def _find_error_handler(self, ex: Exception) -> ErrorHandler[_ReqT, _RespT] | None:
12241221
# NOTE(csojinb): The `__mro__` class attribute returns the method
12251222
# resolution order tuple, i.e. the complete linear inheritance chain
12261223
# ``(type(ex), ..., object)``. For a valid exception class, the last

falcon/app_helpers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from typing import IO, Literal, Optional, overload, Union
2222

2323
from falcon import util
24+
from falcon._typing import _AReqT
25+
from falcon._typing import _ARespT
2426
from falcon._typing import _ReqT
2527
from falcon._typing import _RespT
2628
from falcon._typing import AsgiProcessRequestMethod as APRequest
@@ -75,7 +77,7 @@ def prepare_middleware(
7577

7678
@overload
7779
def prepare_middleware(
78-
middleware: Iterable[AsyncMiddleware],
80+
middleware: Iterable[AsyncMiddleware[_AReqT, _ARespT]],
7981
independent_middleware: bool = ...,
8082
*,
8183
asgi: Literal[True],
@@ -84,14 +86,16 @@ def prepare_middleware(
8486

8587
@overload
8688
def prepare_middleware(
87-
middleware: Iterable[SyncMiddleware[_ReqT, _RespT]] | Iterable[AsyncMiddleware],
89+
middleware: Iterable[SyncMiddleware[_ReqT, _RespT]]
90+
| Iterable[AsyncMiddleware[_AReqT, _ARespT]],
8891
independent_middleware: bool = ...,
8992
asgi: bool = ...,
9093
) -> PreparedMiddlewareResult | AsyncPreparedMiddlewareResult: ...
9194

9295

9396
def prepare_middleware(
94-
middleware: Iterable[SyncMiddleware[_ReqT, _RespT]] | Iterable[AsyncMiddleware],
97+
middleware: Iterable[SyncMiddleware[_ReqT, _RespT]]
98+
| Iterable[AsyncMiddleware[_AReqT, _ARespT]],
9599
independent_middleware: bool = False,
96100
asgi: bool = False,
97101
) -> PreparedMiddlewareResult | AsyncPreparedMiddlewareResult:
@@ -225,7 +229,7 @@ def prepare_middleware(
225229

226230

227231
def prepare_middleware_ws(
228-
middleware: Iterable[AsyncMiddleware],
232+
middleware: Iterable[AsyncMiddleware[_AReqT, _ARespT]],
229233
) -> AsyncPreparedMiddlewareWsResult:
230234
"""Check middleware interfaces and prepare WebSocket methods for request handling.
231235

falcon/asgi/app.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
ClassVar,
2929
overload,
3030
TYPE_CHECKING,
31-
TypeVar,
3231
)
3332
import warnings
3433

3534
from falcon import constants
3635
from falcon import responders
3736
from falcon import routing
37+
from falcon._typing import _AReqT as _ReqT
38+
from falcon._typing import _ARespT as _RespT
39+
from falcon._typing import _ExcT
3840
from falcon._typing import _UNSET
3941
from falcon._typing import AsgiErrorHandler
4042
from falcon._typing import AsgiReceive
@@ -81,7 +83,7 @@
8183
__all__ = ('App',)
8284

8385

84-
# TODO(vytas): Clean up these foul workarounds before the 4.0 release.
86+
# TODO(vytas): Clean up these foul workarounds before the 5.0 release.
8587
MultipartFormHandler._ASGI_MULTIPART_FORM = MultipartForm
8688

8789
_EVT_RESP_EOF: AsgiSendMsg = {'type': EventType.HTTP_RESPONSE_BODY}
@@ -92,10 +94,6 @@
9294

9395
_FALLBACK_WS_ERROR_CODE = 3011
9496

95-
_ExcT = TypeVar('_ExcT', bound=Exception)
96-
_ReqT = TypeVar('_ReqT', bound=Request, contravariant=True)
97-
_RespT = TypeVar('_RespT', bound=Response, contravariant=True)
98-
9997

10098
class App(falcon.app.App[_ReqT, _RespT]):
10199
'''The main entry point into a Falcon-based ASGI app.

0 commit comments

Comments
 (0)