Skip to content

Commit 42f0e2c

Browse files
committed
Add helper to manage aliases and deprecations.
This may save a little bit of CPU and memory by avoiding unnecessary imports too, especially as the library grows.
1 parent 94256f4 commit 42f0e2c

15 files changed

+256
-118
lines changed

src/websockets/__init__.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
# This relies on each of the submodules having an __all__ variable.
2-
3-
from .client import *
4-
from .datastructures import * # noqa
5-
from .exceptions import * # noqa
6-
from .legacy.auth import * # noqa
7-
from .legacy.client import * # noqa
8-
from .legacy.protocol import * # noqa
9-
from .legacy.server import * # noqa
10-
from .server import *
11-
from .typing import * # noqa
12-
from .uri import * # noqa
1+
from .imports import lazy_import
132
from .version import version as __version__ # noqa
143

154

16-
__all__ = [
5+
__all__ = [ # noqa
176
"AbortHandshake",
187
"basic_auth_protocol_factory",
198
"BasicAuthWebSocketServerProtocol",
@@ -58,3 +47,60 @@
5847
"WebSocketServerProtocol",
5948
"WebSocketURI",
6049
]
50+
51+
lazy_import(
52+
globals(),
53+
aliases={
54+
"auth": ".legacy",
55+
"basic_auth_protocol_factory": ".legacy.auth",
56+
"BasicAuthWebSocketServerProtocol": ".legacy.auth",
57+
"ClientConnection": ".client",
58+
"connect": ".legacy.client",
59+
"unix_connect": ".legacy.client",
60+
"WebSocketClientProtocol": ".legacy.client",
61+
"Headers": ".datastructures",
62+
"MultipleValuesError": ".datastructures",
63+
"WebSocketException": ".exceptions",
64+
"ConnectionClosed": ".exceptions",
65+
"ConnectionClosedError": ".exceptions",
66+
"ConnectionClosedOK": ".exceptions",
67+
"InvalidHandshake": ".exceptions",
68+
"SecurityError": ".exceptions",
69+
"InvalidMessage": ".exceptions",
70+
"InvalidHeader": ".exceptions",
71+
"InvalidHeaderFormat": ".exceptions",
72+
"InvalidHeaderValue": ".exceptions",
73+
"InvalidOrigin": ".exceptions",
74+
"InvalidUpgrade": ".exceptions",
75+
"InvalidStatusCode": ".exceptions",
76+
"NegotiationError": ".exceptions",
77+
"DuplicateParameter": ".exceptions",
78+
"InvalidParameterName": ".exceptions",
79+
"InvalidParameterValue": ".exceptions",
80+
"AbortHandshake": ".exceptions",
81+
"RedirectHandshake": ".exceptions",
82+
"InvalidState": ".exceptions",
83+
"InvalidURI": ".exceptions",
84+
"PayloadTooBig": ".exceptions",
85+
"ProtocolError": ".exceptions",
86+
"WebSocketProtocolError": ".exceptions",
87+
"protocol": ".legacy",
88+
"WebSocketCommonProtocol": ".legacy.protocol",
89+
"ServerConnection": ".server",
90+
"serve": ".legacy.server",
91+
"unix_serve": ".legacy.server",
92+
"WebSocketServerProtocol": ".legacy.server",
93+
"WebSocketServer": ".legacy.server",
94+
"Data": ".typing",
95+
"Origin": ".typing",
96+
"ExtensionHeader": ".typing",
97+
"ExtensionParameter": ".typing",
98+
"Subprotocol": ".typing",
99+
"parse_uri": ".uri",
100+
"WebSocketURI": ".uri",
101+
},
102+
deprecated_aliases={
103+
"framing": ".legacy",
104+
"handshake": ".legacy",
105+
},
106+
)

src/websockets/auth.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/websockets/client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from .http import USER_AGENT, build_host
2626
from .http11 import Request, Response
27-
from .legacy.client import WebSocketClientProtocol, connect, unix_connect # noqa
27+
from .imports import lazy_import
2828
from .typing import (
2929
ConnectionOption,
3030
ExtensionHeader,
@@ -36,6 +36,15 @@
3636
from .utils import accept_key, generate_key
3737

3838

39+
lazy_import(
40+
globals(),
41+
aliases={
42+
"connect": ".legacy.client",
43+
"unix_connect": ".legacy.client",
44+
"WebSocketClientProtocol": ".legacy.client",
45+
},
46+
)
47+
3948
__all__ = ["ClientConnection"]
4049

4150
logger = logging.getLogger(__name__)

src/websockets/framing.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/websockets/handshake.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/websockets/http.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
import asyncio
21
import ipaddress
32
import sys
4-
import warnings
5-
from typing import Tuple
63

7-
# For backwards compatibility:
8-
# Headers and MultipleValuesError used to be defined in this module
9-
from .datastructures import Headers, MultipleValuesError # noqa
4+
from .imports import lazy_import
105
from .version import version as websockets_version
116

127

8+
# For backwards compatibility:
9+
10+
11+
lazy_import(
12+
globals(),
13+
# Headers and MultipleValuesError used to be defined in this module.
14+
aliases={
15+
"Headers": ".datastructures",
16+
"MultipleValuesError": ".datastructures",
17+
},
18+
deprecated_aliases={
19+
"read_request": ".legacy.http",
20+
"read_response": ".legacy.http",
21+
},
22+
)
23+
24+
1325
__all__ = ["USER_AGENT", "build_host"]
1426

1527

@@ -38,24 +50,3 @@ def build_host(host: str, port: int, secure: bool) -> str:
3850
host = f"{host}:{port}"
3951

4052
return host
41-
42-
43-
# Backwards compatibility with previously documented public APIs
44-
45-
46-
async def read_request(
47-
stream: asyncio.StreamReader,
48-
) -> Tuple[str, Headers]: # pragma: no cover
49-
warnings.warn("websockets.http.read_request is deprecated", DeprecationWarning)
50-
from .legacy.http import read_request
51-
52-
return await read_request(stream)
53-
54-
55-
async def read_response(
56-
stream: asyncio.StreamReader,
57-
) -> Tuple[int, str, Headers]: # pragma: no cover
58-
warnings.warn("websockets.http.read_response is deprecated", DeprecationWarning)
59-
from .legacy.http import read_response
60-
61-
return await read_response(stream)

src/websockets/imports.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import importlib
2+
import sys
3+
import warnings
4+
from typing import Any, Dict, Iterable, Optional
5+
6+
7+
__all__ = ["lazy_import"]
8+
9+
10+
def lazy_import(
11+
namespace: Dict[str, Any],
12+
aliases: Optional[Dict[str, str]] = None,
13+
deprecated_aliases: Optional[Dict[str, str]] = None,
14+
) -> None:
15+
"""
16+
Provide lazy, module-level imports.
17+
18+
Typical use::
19+
20+
__getattr__, __dir__ = lazy_import(
21+
globals(),
22+
aliases={
23+
"<name>": "<source module>",
24+
...
25+
},
26+
deprecated_aliases={
27+
...,
28+
}
29+
)
30+
31+
This function defines __getattr__ and __dir__ per PEP 562.
32+
33+
On Python 3.6 and earlier, it falls back to non-lazy imports and doesn't
34+
raise deprecation warnings.
35+
36+
"""
37+
if aliases is None:
38+
aliases = {}
39+
if deprecated_aliases is None:
40+
deprecated_aliases = {}
41+
42+
namespace_set = set(namespace)
43+
aliases_set = set(aliases)
44+
deprecated_aliases_set = set(deprecated_aliases)
45+
46+
assert not namespace_set & aliases_set, "namespace conflict"
47+
assert not namespace_set & deprecated_aliases_set, "namespace conflict"
48+
assert not aliases_set & deprecated_aliases_set, "namespace conflict"
49+
50+
package = namespace["__name__"]
51+
52+
if sys.version_info[:2] >= (3, 7):
53+
54+
def __getattr__(name: str) -> Any:
55+
assert aliases is not None # mypy cannot figure this out
56+
try:
57+
source = aliases[name]
58+
except KeyError:
59+
pass
60+
else:
61+
module = importlib.import_module(source, package)
62+
return getattr(module, name)
63+
64+
assert deprecated_aliases is not None # mypy cannot figure this out
65+
try:
66+
source = deprecated_aliases[name]
67+
except KeyError:
68+
pass
69+
else:
70+
warnings.warn(
71+
f"{package}.{name} is deprecated",
72+
DeprecationWarning,
73+
stacklevel=2,
74+
)
75+
module = importlib.import_module(source, package)
76+
return getattr(module, name)
77+
78+
raise AttributeError(f"module {package!r} has no attribute {name!r}")
79+
80+
namespace["__getattr__"] = __getattr__
81+
82+
def __dir__() -> Iterable[str]:
83+
return sorted(namespace_set | aliases_set | deprecated_aliases_set)
84+
85+
namespace["__dir__"] = __dir__
86+
87+
else: # pragma: no cover
88+
89+
for name, source in aliases.items():
90+
module = importlib.import_module(source, package)
91+
namespace[name] = getattr(module, name)
92+
93+
for name, source in deprecated_aliases.items():
94+
module = importlib.import_module(source, package)
95+
namespace[name] = getattr(module, name)

src/websockets/protocol.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/websockets/server.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,7 @@
2626
)
2727
from .http import USER_AGENT
2828
from .http11 import Request, Response
29-
from .legacy.server import ( # noqa
30-
WebSocketServer,
31-
WebSocketServerProtocol,
32-
serve,
33-
unix_serve,
34-
)
29+
from .imports import lazy_import
3530
from .typing import (
3631
ConnectionOption,
3732
ExtensionHeader,
@@ -42,6 +37,17 @@
4237
from .utils import accept_key
4338

4439

40+
lazy_import(
41+
globals(),
42+
aliases={
43+
"serve": ".legacy.server",
44+
"unix_serve": ".legacy.server",
45+
"WebSocketServerProtocol": ".legacy.server",
46+
"WebSocketServer": ".legacy.server",
47+
},
48+
)
49+
50+
4551
__all__ = ["ServerConnection"]
4652

4753
logger = logging.getLogger(__name__)

tests/test_auth.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)