Skip to content

Commit 2843f67

Browse files
authored
Renamed Websocket* types to WebSocket*
This matches our usage of the noun elsewhere in specs, and the official spelling.
1 parent 7c24756 commit 2843f67

4 files changed

Lines changed: 172 additions & 21 deletions

File tree

asgiref/_pep562.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Backport of PEP 562.
3+
https://pypi.org/search/?q=pep562
4+
Licensed under MIT
5+
Copyright (c) 2018 Isaac Muse <[email protected]>
6+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
9+
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10+
The above copyright notice and this permission notice shall be included in all copies or substantial portions
11+
of the Software.
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
13+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
14+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
15+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
16+
IN THE SOFTWARE.
17+
"""
18+
import sys
19+
from typing import Any, Callable, List, Optional
20+
21+
22+
class Pep562:
23+
"""
24+
Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
25+
Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
26+
The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
27+
"""
28+
29+
def __init__(self, name: str) -> None:
30+
"""Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
31+
32+
self._module = sys.modules[name]
33+
self._get_attr = getattr(self._module, "__getattr__", None)
34+
self._get_dir: Optional[Callable[..., List[str]]] = getattr(
35+
self._module, "__dir__", None
36+
)
37+
sys.modules[name] = self # type: ignore[assignment]
38+
39+
def __dir__(self) -> List[str]:
40+
"""Return the overridden `dir` if one was provided, else apply `dir` to the module."""
41+
42+
return self._get_dir() if self._get_dir else dir(self._module)
43+
44+
def __getattr__(self, name: str) -> Any:
45+
"""
46+
Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present.
47+
"""
48+
49+
try:
50+
return getattr(self._module, name)
51+
except AttributeError:
52+
if self._get_attr:
53+
return self._get_attr(name)
54+
raise
55+
56+
57+
def pep562(module_name: str) -> None:
58+
"""Helper function to apply PEP 562."""
59+
60+
if sys.version_info < (3, 7):
61+
Pep562(module_name)

asgiref/typing.py

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,50 @@
11
import sys
2-
from typing import Awaitable, Callable, Dict, Iterable, Optional, Tuple, Type, Union
2+
import warnings
3+
from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union
4+
5+
from asgiref._pep562 import pep562
36

47
if sys.version_info >= (3, 8):
58
from typing import Literal, Protocol, TypedDict
69
else:
710
from typing_extensions import Literal, Protocol, TypedDict
811

12+
__all__ = (
13+
"ASGIVersions",
14+
"HTTPScope",
15+
"WebSocketScope",
16+
"LifespanScope",
17+
"WWWScope",
18+
"Scope",
19+
"HTTPRequestEvent",
20+
"HTTPResponseStartEvent",
21+
"HTTPResponseBodyEvent",
22+
"HTTPServerPushEvent",
23+
"HTTPDisconnectEvent",
24+
"WebSocketConnectEvent",
25+
"WebSocketAcceptEvent",
26+
"WebSocketReceiveEvent",
27+
"WebSocketSendEvent",
28+
"WebSocketResponseStartEvent",
29+
"WebSocketResponseBodyEvent",
30+
"WebSocketDisconnectEvent",
31+
"WebSocketCloseEvent",
32+
"LifespanStartupEvent",
33+
"LifespanShutdownEvent",
34+
"LifespanStartupCompleteEvent",
35+
"LifespanStartupFailedEvent",
36+
"LifespanShutdownCompleteEvent",
37+
"LifespanShutdownFailedEvent",
38+
"ASGIReceiveEvent",
39+
"ASGISendEvent",
40+
"ASGIReceiveCallable",
41+
"ASGISendCallable",
42+
"ASGI2Protocol",
43+
"ASGI2Application",
44+
"ASGI3Application",
45+
"ASGIApplication",
46+
)
47+
948

1049
class ASGIVersions(TypedDict):
1150
spec_version: str
@@ -28,7 +67,7 @@ class HTTPScope(TypedDict):
2867
extensions: Optional[Dict[str, Dict[object, object]]]
2968

3069

31-
class WebsocketScope(TypedDict):
70+
class WebSocketScope(TypedDict):
3271
type: Literal["websocket"]
3372
asgi: ASGIVersions
3473
http_version: str
@@ -49,8 +88,8 @@ class LifespanScope(TypedDict):
4988
asgi: ASGIVersions
5089

5190

52-
WWWScope = Union[HTTPScope, WebsocketScope]
53-
Scope = Union[HTTPScope, WebsocketScope, LifespanScope]
91+
WWWScope = Union[HTTPScope, WebSocketScope]
92+
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
5493

5594

5695
class HTTPRequestEvent(TypedDict):
@@ -81,46 +120,46 @@ class HTTPDisconnectEvent(TypedDict):
81120
type: Literal["http.disconnect"]
82121

83122

84-
class WebsocketConnectEvent(TypedDict):
123+
class WebSocketConnectEvent(TypedDict):
85124
type: Literal["websocket.connect"]
86125

87126

88-
class WebsocketAcceptEvent(TypedDict):
127+
class WebSocketAcceptEvent(TypedDict):
89128
type: Literal["websocket.accept"]
90129
subprotocol: Optional[str]
91130
headers: Iterable[Tuple[bytes, bytes]]
92131

93132

94-
class WebsocketReceiveEvent(TypedDict):
133+
class WebSocketReceiveEvent(TypedDict):
95134
type: Literal["websocket.receive"]
96135
bytes: Optional[bytes]
97136
text: Optional[str]
98137

99138

100-
class WebsocketSendEvent(TypedDict):
139+
class WebSocketSendEvent(TypedDict):
101140
type: Literal["websocket.send"]
102141
bytes: Optional[bytes]
103142
text: Optional[str]
104143

105144

106-
class WebsocketResponseStartEvent(TypedDict):
145+
class WebSocketResponseStartEvent(TypedDict):
107146
type: Literal["websocket.http.response.start"]
108147
status: int
109148
headers: Iterable[Tuple[bytes, bytes]]
110149

111150

112-
class WebsocketResponseBodyEvent(TypedDict):
151+
class WebSocketResponseBodyEvent(TypedDict):
113152
type: Literal["websocket.http.response.body"]
114153
body: bytes
115154
more_body: bool
116155

117156

118-
class WebsocketDisconnectEvent(TypedDict):
157+
class WebSocketDisconnectEvent(TypedDict):
119158
type: Literal["websocket.disconnect"]
120159
code: int
121160

122161

123-
class WebsocketCloseEvent(TypedDict):
162+
class WebSocketCloseEvent(TypedDict):
124163
type: Literal["websocket.close"]
125164
code: int
126165
reason: Optional[str]
@@ -155,9 +194,9 @@ class LifespanShutdownFailedEvent(TypedDict):
155194
ASGIReceiveEvent = Union[
156195
HTTPRequestEvent,
157196
HTTPDisconnectEvent,
158-
WebsocketConnectEvent,
159-
WebsocketReceiveEvent,
160-
WebsocketDisconnectEvent,
197+
WebSocketConnectEvent,
198+
WebSocketReceiveEvent,
199+
WebSocketDisconnectEvent,
161200
LifespanStartupEvent,
162201
LifespanShutdownEvent,
163202
]
@@ -168,11 +207,11 @@ class LifespanShutdownFailedEvent(TypedDict):
168207
HTTPResponseBodyEvent,
169208
HTTPServerPushEvent,
170209
HTTPDisconnectEvent,
171-
WebsocketAcceptEvent,
172-
WebsocketSendEvent,
173-
WebsocketResponseStartEvent,
174-
WebsocketResponseBodyEvent,
175-
WebsocketCloseEvent,
210+
WebSocketAcceptEvent,
211+
WebSocketSendEvent,
212+
WebSocketResponseStartEvent,
213+
WebSocketResponseBodyEvent,
214+
WebSocketCloseEvent,
176215
LifespanStartupCompleteEvent,
177216
LifespanStartupFailedEvent,
178217
LifespanShutdownCompleteEvent,
@@ -204,3 +243,34 @@ async def __call__(
204243
Awaitable[None],
205244
]
206245
ASGIApplication = Union[ASGI2Application, ASGI3Application]
246+
247+
__deprecated__ = {
248+
"WebsocketConnectEvent": WebSocketConnectEvent,
249+
"WebsocketAcceptEvent": WebSocketAcceptEvent,
250+
"WebsocketReceiveEvent": WebSocketReceiveEvent,
251+
"WebsocketSendEvent": WebSocketSendEvent,
252+
"WebsocketResponseStartEvent": WebSocketResponseStartEvent,
253+
"WebsocketResponseBodyEvent": WebSocketResponseBodyEvent,
254+
"WebsocketDisconnectEvent": WebSocketDisconnectEvent,
255+
"WebsocketCloseEvent": WebSocketCloseEvent,
256+
}
257+
258+
259+
def __getattr__(name: str) -> Any:
260+
deprecated = __deprecated__.get(name)
261+
if deprecated:
262+
stacklevel = 3 if sys.version_info >= (3, 7) else 4
263+
warnings.warn(
264+
f"'{name}' is deprecated. Use '{deprecated.__name__}' instead.",
265+
category=DeprecationWarning,
266+
stacklevel=stacklevel,
267+
)
268+
return deprecated
269+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
270+
271+
272+
def __dir__() -> List[str]:
273+
return sorted(list(__all__) + list(__deprecated__.keys()))
274+
275+
276+
pep562(__name__)

tests/test_deprecated_types.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import importlib
2+
3+
import pytest
4+
5+
from asgiref import typing
6+
7+
8+
@pytest.mark.parametrize("deprecated_type", typing.__deprecated__.keys())
9+
def test_deprecated_types(deprecated_type: str) -> None:
10+
with pytest.warns(DeprecationWarning) as record:
11+
getattr(importlib.import_module("asgiref.typing"), deprecated_type)
12+
assert len(record) == 1
13+
assert deprecated_type in str(record.list[0])
14+
15+
16+
@pytest.mark.parametrize("available_type", typing.__all__)
17+
def test_available_types(available_type: str) -> None:
18+
with pytest.warns(None) as record:
19+
getattr(importlib.import_module("asgiref.typing"), available_type)
20+
assert len(record) == 0

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ commands =
1111
mypy: mypy . {posargs}
1212

1313
[testenv:qa]
14-
skip_install=true
14+
skip_install = true
1515
deps =
1616
pre-commit
1717
commands =

0 commit comments

Comments
 (0)