Skip to content

Commit 50b6525

Browse files
authored
fix(typing): include protocols for WebSocket middleware + cleanup (#2472)
Follow-up to #2390.
1 parent 18d0851 commit 50b6525

6 files changed

Lines changed: 67 additions & 35 deletions

File tree

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125
# TODO(vytas): If we enable the "nitpicky" mode, we will have to add exceptions
126126
# for all unresolved aliases.
127127
autodoc_type_aliases = {
128+
'SyncMiddleware': 'SyncMiddleware',
129+
'AsyncMiddleware': 'AsyncMiddleware',
128130
'PreparedMiddlewareResult': 'PreparedMiddlewareResult',
129131
}
130132

falcon/_typing.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,14 @@ def __call__(self, media: Any, content_type: Optional[str] = ...) -> bytes: ...
200200
Responder = Union[ResponderMethod, AsgiResponderMethod]
201201

202202

203-
# Middleware
204-
class MiddlewareWithProcessRequest(Protocol):
203+
# WSGI middleware interface
204+
class WsgiMiddlewareWithProcessRequest(Protocol):
205205
"""WSGI Middleware with request handler."""
206206

207207
def process_request(self, req: Request, resp: Response) -> None: ...
208208

209209

210-
class MiddlewareWithProcessResource(Protocol):
210+
class WsgiMiddlewareWithProcessResource(Protocol):
211211
"""WSGI Middleware with resource handler."""
212212

213213
def process_resource(
@@ -219,14 +219,15 @@ def process_resource(
219219
) -> None: ...
220220

221221

222-
class MiddlewareWithProcessResponse(Protocol):
222+
class WsgiMiddlewareWithProcessResponse(Protocol):
223223
"""WSGI Middleware with response handler."""
224224

225225
def process_response(
226226
self, req: Request, resp: Response, resource: object, req_succeeded: bool
227227
) -> None: ...
228228

229229

230+
# ASGI lifespan middleware interface
230231
class AsgiMiddlewareWithProcessStartup(Protocol):
231232
"""ASGI middleware with startup handler."""
232233

@@ -243,6 +244,7 @@ async def process_shutdown(
243244
) -> None: ...
244245

245246

247+
# ASGI middleware interface
246248
class AsgiMiddlewareWithProcessRequest(Protocol):
247249
"""ASGI middleware with request handler."""
248250

@@ -273,13 +275,14 @@ async def process_response(
273275
) -> None: ...
274276

275277

276-
class MiddlewareWithAsyncProcessRequestWs(Protocol):
278+
# ASGI WebSocket middleware
279+
class AsgiMiddlewareWithProcessRequestWs(Protocol):
277280
"""ASGI middleware with WebSocket request handler."""
278281

279282
async def process_request_ws(self, req: AsgiRequest, ws: WebSocket) -> None: ...
280283

281284

282-
class MiddlewareWithAsyncProcessResourceWs(Protocol):
285+
class AsgiMiddlewareWithProcessResourceWs(Protocol):
283286
"""ASGI middleware with WebSocket resource handler."""
284287

285288
async def process_resource_ws(
@@ -291,15 +294,18 @@ async def process_resource_ws(
291294
) -> None: ...
292295

293296

294-
class UniversalMiddlewareWithProcessRequest(MiddlewareWithProcessRequest, Protocol):
297+
# Universal middleware that provides async versions via the _async postfix
298+
class UniversalMiddlewareWithProcessRequest(WsgiMiddlewareWithProcessRequest, Protocol):
295299
"""WSGI/ASGI middleware with request handler."""
296300

297301
async def process_request_async(
298302
self, req: AsgiRequest, resp: AsgiResponse
299303
) -> None: ...
300304

301305

302-
class UniversalMiddlewareWithProcessResource(MiddlewareWithProcessResource, Protocol):
306+
class UniversalMiddlewareWithProcessResource(
307+
WsgiMiddlewareWithProcessResource, Protocol
308+
):
303309
"""WSGI/ASGI middleware with resource handler."""
304310

305311
async def process_resource_async(
@@ -311,7 +317,9 @@ async def process_resource_async(
311317
) -> None: ...
312318

313319

314-
class UniversalMiddlewareWithProcessResponse(MiddlewareWithProcessResponse, Protocol):
320+
class UniversalMiddlewareWithProcessResponse(
321+
WsgiMiddlewareWithProcessResponse, Protocol
322+
):
315323
"""WSGI/ASGI middleware with response handler."""
316324

317325
async def process_response_async(
@@ -326,19 +334,34 @@ async def process_response_async(
326334
# NOTE(jkmnt): This typing is far from perfect due to the Python typing limitations,
327335
# but better than nothing. Middleware conforming to any protocol of the union
328336
# will pass the type check. Other protocols violations are not checked.
329-
Middleware = Union[
330-
MiddlewareWithProcessRequest,
331-
MiddlewareWithProcessResource,
332-
MiddlewareWithProcessResponse,
337+
SyncMiddleware = Union[
338+
WsgiMiddlewareWithProcessRequest,
339+
WsgiMiddlewareWithProcessResource,
340+
WsgiMiddlewareWithProcessResponse,
333341
]
342+
"""Synchronous (WSGI) application middleware.
334343
335-
AsgiMiddleware = Union[
344+
This type alias reflects the middleware interface for
345+
components that can be used with a WSGI app.
346+
"""
347+
348+
AsyncMiddleware = Union[
336349
AsgiMiddlewareWithProcessRequest,
337350
AsgiMiddlewareWithProcessResource,
338351
AsgiMiddlewareWithProcessResponse,
352+
# Lifespan middleware
339353
AsgiMiddlewareWithProcessStartup,
340354
AsgiMiddlewareWithProcessShutdown,
355+
# WebSocket middleware
356+
AsgiMiddlewareWithProcessRequestWs,
357+
AsgiMiddlewareWithProcessResourceWs,
358+
# Universal middleware with process_*_async methods
341359
UniversalMiddlewareWithProcessRequest,
342360
UniversalMiddlewareWithProcessResource,
343361
UniversalMiddlewareWithProcessResponse,
344362
]
363+
"""Asynchronous (ASGI) application middleware.
364+
365+
This type alias reflects the middleware interface for components that can be
366+
used with an ASGI app.
367+
"""

falcon/app.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@
5151
from falcon._typing import ErrorHandler
5252
from falcon._typing import ErrorSerializer
5353
from falcon._typing import FindMethod
54-
from falcon._typing import Middleware
5554
from falcon._typing import ProcessResponseMethod
5655
from falcon._typing import ResponderCallable
5756
from falcon._typing import SinkCallable
5857
from falcon._typing import SinkPrefix
5958
from falcon._typing import StartResponse
59+
from falcon._typing import SyncMiddleware
6060
from falcon._typing import WSGIEnvironment
6161
from falcon.errors import CompatibilityError
6262
from falcon.errors import HTTPBadRequest
@@ -287,7 +287,7 @@ def process_response(
287287
_static_routes: List[
288288
Tuple[routing.StaticRoute, routing.StaticRoute, Literal[False]]
289289
]
290-
_unprepared_middleware: List[Middleware]
290+
_unprepared_middleware: List[SyncMiddleware]
291291

292292
# Attributes
293293
req_options: RequestOptions
@@ -306,7 +306,7 @@ def __init__(
306306
media_type: str = constants.DEFAULT_MEDIA_TYPE,
307307
request_type: Optional[Type[Request]] = None,
308308
response_type: Optional[Type[Response]] = None,
309-
middleware: Optional[Union[Middleware, Iterable[Middleware]]] = None,
309+
middleware: Optional[Union[SyncMiddleware, Iterable[SyncMiddleware]]] = None,
310310
router: Optional[routing.CompiledRouter] = None,
311311
independent_middleware: bool = True,
312312
cors_enable: bool = False,
@@ -328,12 +328,12 @@ def __init__(
328328
# NOTE(kgriffs): Check to see if middleware is an
329329
# iterable, and if so, append the CORSMiddleware
330330
# instance.
331-
middleware = list(cast(Iterable[Middleware], middleware))
331+
middleware = list(cast(Iterable[SyncMiddleware], middleware))
332332
middleware.append(cm)
333333
except TypeError:
334334
# NOTE(kgriffs): Assume the middleware kwarg references
335335
# a single middleware component.
336-
middleware = [cast(Middleware, middleware), cm]
336+
middleware = [cast(SyncMiddleware, middleware), cm]
337337

338338
# set middleware
339339
self._unprepared_middleware = []
@@ -526,7 +526,7 @@ def router_options(self) -> routing.CompiledRouterOptions:
526526
return self._router.options
527527

528528
def add_middleware(
529-
self, middleware: Union[Middleware, Iterable[Middleware]]
529+
self, middleware: Union[SyncMiddleware, Iterable[SyncMiddleware]]
530530
) -> None:
531531
"""Add one or more additional middleware components.
532532
@@ -541,10 +541,10 @@ def add_middleware(
541541
# the chance that middleware may be empty.
542542
if middleware:
543543
try:
544-
middleware = list(cast(Iterable[Middleware], middleware))
544+
middleware = list(cast(Iterable[SyncMiddleware], middleware))
545545
except TypeError:
546546
# middleware is not iterable; assume it is just one bare component
547-
middleware = [cast(Middleware, middleware)]
547+
middleware = [cast(SyncMiddleware, middleware)]
548548

549549
if (
550550
self._cors_enable
@@ -1015,7 +1015,7 @@ def my_serializer(
10151015
# ------------------------------------------------------------------------
10161016

10171017
def _prepare_middleware(
1018-
self, middleware: List[Middleware], independent_middleware: bool = False
1018+
self, middleware: List[SyncMiddleware], independent_middleware: bool = False
10191019
) -> helpers.PreparedMiddlewareResult:
10201020
return helpers.prepare_middleware(
10211021
middleware=middleware, independent_middleware=independent_middleware

falcon/app_helpers.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@
2020
from typing import IO, Iterable, List, Literal, Optional, overload, Tuple, Union
2121

2222
from falcon import util
23-
from falcon._typing import AsgiMiddleware
2423
from falcon._typing import AsgiProcessRequestMethod as APRequest
2524
from falcon._typing import AsgiProcessRequestWsMethod
2625
from falcon._typing import AsgiProcessResourceMethod as APResource
2726
from falcon._typing import AsgiProcessResourceWsMethod
2827
from falcon._typing import AsgiProcessResponseMethod as APResponse
29-
from falcon._typing import Middleware
28+
from falcon._typing import AsyncMiddleware
3029
from falcon._typing import ProcessRequestMethod as PRequest
3130
from falcon._typing import ProcessResourceMethod as PResource
3231
from falcon._typing import ProcessResponseMethod as PResponse
32+
from falcon._typing import SyncMiddleware
3333
from falcon.constants import MEDIA_JSON
3434
from falcon.constants import MEDIA_XML
3535
from falcon.errors import CompatibilityError
@@ -64,15 +64,15 @@
6464

6565
@overload
6666
def prepare_middleware(
67-
middleware: Iterable[Middleware],
67+
middleware: Iterable[SyncMiddleware],
6868
independent_middleware: bool = ...,
6969
asgi: Literal[False] = ...,
7070
) -> PreparedMiddlewareResult: ...
7171

7272

7373
@overload
7474
def prepare_middleware(
75-
middleware: Iterable[AsgiMiddleware],
75+
middleware: Iterable[AsyncMiddleware],
7676
independent_middleware: bool = ...,
7777
*,
7878
asgi: Literal[True],
@@ -81,14 +81,14 @@ def prepare_middleware(
8181

8282
@overload
8383
def prepare_middleware(
84-
middleware: Union[Iterable[Middleware], Iterable[AsgiMiddleware]],
84+
middleware: Union[Iterable[SyncMiddleware], Iterable[AsyncMiddleware]],
8585
independent_middleware: bool = ...,
8686
asgi: bool = ...,
8787
) -> Union[PreparedMiddlewareResult, AsyncPreparedMiddlewareResult]: ...
8888

8989

9090
def prepare_middleware(
91-
middleware: Union[Iterable[Middleware], Iterable[AsgiMiddleware]],
91+
middleware: Union[Iterable[SyncMiddleware], Iterable[AsyncMiddleware]],
9292
independent_middleware: bool = False,
9393
asgi: bool = False,
9494
) -> Union[PreparedMiddlewareResult, AsyncPreparedMiddlewareResult]:
@@ -223,7 +223,7 @@ def prepare_middleware(
223223

224224

225225
def prepare_middleware_ws(
226-
middleware: Iterable[AsgiMiddleware],
226+
middleware: Iterable[AsyncMiddleware],
227227
) -> AsyncPreparedMiddlewareWsResult:
228228
"""Check middleware interfaces and prepare WebSocket methods for request handling.
229229

falcon/asgi/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@
4242
from falcon import routing
4343
from falcon._typing import _UNSET
4444
from falcon._typing import AsgiErrorHandler
45-
from falcon._typing import AsgiMiddleware
4645
from falcon._typing import AsgiReceive
4746
from falcon._typing import AsgiResponderCallable
4847
from falcon._typing import AsgiResponderWsCallable
4948
from falcon._typing import AsgiSend
5049
from falcon._typing import AsgiSinkCallable
50+
from falcon._typing import AsyncMiddleware
5151
from falcon._typing import SinkPrefix
5252
import falcon.app
5353
from falcon.app_helpers import AsyncPreparedMiddlewareResult
@@ -357,7 +357,7 @@ async def process_resource_ws(
357357
_middleware_ws: AsyncPreparedMiddlewareWsResult
358358
_request_type: Type[Request]
359359
_response_type: Type[Response]
360-
_unprepared_middleware: List[AsgiMiddleware] # type: ignore[assignment]
360+
_unprepared_middleware: List[AsyncMiddleware] # type: ignore[assignment]
361361

362362
ws_options: WebSocketOptions
363363
"""A set of behavioral options related to WebSocket connections.
@@ -370,7 +370,7 @@ def __init__(
370370
media_type: str = constants.DEFAULT_MEDIA_TYPE,
371371
request_type: Optional[Type[Request]] = None,
372372
response_type: Optional[Type[Response]] = None,
373-
middleware: Optional[Union[AsgiMiddleware, Iterable[AsgiMiddleware]]] = None,
373+
middleware: Optional[Union[AsyncMiddleware, Iterable[AsyncMiddleware]]] = None,
374374
router: Optional[routing.CompiledRouter] = None,
375375
independent_middleware: bool = True,
376376
cors_enable: bool = False,
@@ -1165,7 +1165,7 @@ async def _handle_websocket(
11651165
raise
11661166

11671167
def _prepare_middleware( # type: ignore[override]
1168-
self, middleware: List[AsgiMiddleware], independent_middleware: bool = False
1168+
self, middleware: List[AsyncMiddleware], independent_middleware: bool = False
11691169
) -> AsyncPreparedMiddlewareResult:
11701170
self._middleware_ws = prepare_middleware_ws(middleware)
11711171

falcon/typing.py

Lines changed: 8 additions & 1 deletion
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-
"""Module that defines public Falcon type definitions."""
14+
"""Public Falcon type alias definitions."""
1515

1616
from __future__ import annotations
1717

@@ -20,6 +20,13 @@
2020
if TYPE_CHECKING:
2121
from falcon.asgi import SSEvent
2222

23+
__all__ = (
24+
'Headers',
25+
'ReadableIO',
26+
'AsyncReadableIO',
27+
'SSEEmitter',
28+
)
29+
2330
Headers = Dict[str, str]
2431
"""Headers dictionary returned by the framework.
2532

0 commit comments

Comments
 (0)