Skip to content

Commit 3128f56

Browse files
committed
Fix help(websockets) when werkzeug isn't installed.
Fix #1621.
1 parent 77cf665 commit 3128f56

File tree

4 files changed

+361
-315
lines changed

4 files changed

+361
-315
lines changed

src/websockets/asyncio/client.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,16 @@ def unix_connect(
671671
from python_socks import ProxyType
672672
from python_socks.async_.asyncio import Proxy as SocksProxy
673673

674+
except ImportError:
675+
676+
async def connect_socks_proxy(
677+
proxy: Proxy,
678+
ws_uri: WebSocketURI,
679+
**kwargs: Any,
680+
) -> socket.socket:
681+
raise ImportError("connecting through a SOCKS proxy requires python-socks")
682+
683+
else:
674684
SOCKS_PROXY_TYPES = {
675685
"socks5h": ProxyType.SOCKS5,
676686
"socks5": ProxyType.SOCKS5,
@@ -709,15 +719,6 @@ async def connect_socks_proxy(
709719
except Exception as exc:
710720
raise ProxyError("failed to connect to SOCKS proxy") from exc
711721

712-
except ImportError:
713-
714-
async def connect_socks_proxy(
715-
proxy: Proxy,
716-
ws_uri: WebSocketURI,
717-
**kwargs: Any,
718-
) -> socket.socket:
719-
raise ImportError("python-socks is required to use a SOCKS proxy")
720-
721722

722723
def prepare_connect_request(
723724
proxy: Proxy,

src/websockets/asyncio/router.py

+172-150
Original file line numberDiff line numberDiff line change
@@ -5,194 +5,216 @@
55
import urllib.parse
66
from typing import Any, Awaitable, Callable, Literal
77

8-
from werkzeug.exceptions import NotFound
9-
from werkzeug.routing import Map, RequestRedirect
10-
118
from ..http11 import Request, Response
129
from .server import Server, ServerConnection, serve
1310

1411

1512
__all__ = ["route", "unix_route", "Router"]
1613

1714

18-
class Router:
19-
"""WebSocket router supporting :func:`route`."""
15+
try:
16+
from werkzeug.exceptions import NotFound
17+
from werkzeug.routing import Map, RequestRedirect
18+
19+
except ImportError:
2020

21-
def __init__(
22-
self,
21+
def route(
2322
url_map: Map,
23+
*args: Any,
2424
server_name: str | None = None,
25-
url_scheme: str = "ws",
26-
) -> None:
27-
self.url_map = url_map
28-
self.server_name = server_name
29-
self.url_scheme = url_scheme
30-
for rule in self.url_map.iter_rules():
31-
rule.websocket = True
32-
33-
def get_server_name(self, connection: ServerConnection, request: Request) -> str:
34-
if self.server_name is None:
35-
return request.headers["Host"]
36-
else:
37-
return self.server_name
38-
39-
def redirect(self, connection: ServerConnection, url: str) -> Response:
40-
response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}")
41-
response.headers["Location"] = url
42-
return response
43-
44-
def not_found(self, connection: ServerConnection) -> Response:
45-
return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found")
46-
47-
def route_request(
48-
self, connection: ServerConnection, request: Request
49-
) -> Response | None:
50-
"""Route incoming request."""
51-
url_map_adapter = self.url_map.bind(
52-
server_name=self.get_server_name(connection, request),
53-
url_scheme=self.url_scheme,
54-
)
55-
try:
56-
parsed = urllib.parse.urlparse(request.path)
57-
handler, kwargs = url_map_adapter.match(
58-
path_info=parsed.path,
59-
query_args=parsed.query,
60-
)
61-
except RequestRedirect as redirect:
62-
return self.redirect(connection, redirect.new_url)
63-
except NotFound:
64-
return self.not_found(connection)
65-
connection.handler, connection.handler_kwargs = handler, kwargs
66-
return None
25+
ssl: ssl_module.SSLContext | Literal[True] | None = None,
26+
create_router: type[Router] | None = None,
27+
**kwargs: Any,
28+
) -> Awaitable[Server]:
29+
raise ImportError("route() requires werkzeug")
6730

68-
async def handler(self, connection: ServerConnection) -> None:
69-
"""Handle a connection."""
70-
return await connection.handler(connection, **connection.handler_kwargs)
71-
72-
73-
def route(
74-
url_map: Map,
75-
*args: Any,
76-
server_name: str | None = None,
77-
ssl: ssl_module.SSLContext | Literal[True] | None = None,
78-
create_router: type[Router] | None = None,
79-
**kwargs: Any,
80-
) -> Awaitable[Server]:
81-
"""
82-
Create a WebSocket server dispatching connections to different handlers.
83-
84-
This feature requires the third-party library `werkzeug`_:
31+
def unix_route(
32+
url_map: Map,
33+
path: str | None = None,
34+
**kwargs: Any,
35+
) -> Awaitable[Server]:
36+
raise ImportError("unix_route() requires werkzeug")
37+
38+
else:
39+
40+
class Router:
41+
"""WebSocket router supporting :func:`route`."""
42+
43+
def __init__(
44+
self,
45+
url_map: Map,
46+
server_name: str | None = None,
47+
url_scheme: str = "ws",
48+
) -> None:
49+
self.url_map = url_map
50+
self.server_name = server_name
51+
self.url_scheme = url_scheme
52+
for rule in self.url_map.iter_rules():
53+
rule.websocket = True
54+
55+
def get_server_name(
56+
self, connection: ServerConnection, request: Request
57+
) -> str:
58+
if self.server_name is None:
59+
return request.headers["Host"]
60+
else:
61+
return self.server_name
62+
63+
def redirect(self, connection: ServerConnection, url: str) -> Response:
64+
response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}")
65+
response.headers["Location"] = url
66+
return response
67+
68+
def not_found(self, connection: ServerConnection) -> Response:
69+
return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found")
70+
71+
def route_request(
72+
self, connection: ServerConnection, request: Request
73+
) -> Response | None:
74+
"""Route incoming request."""
75+
url_map_adapter = self.url_map.bind(
76+
server_name=self.get_server_name(connection, request),
77+
url_scheme=self.url_scheme,
78+
)
79+
try:
80+
parsed = urllib.parse.urlparse(request.path)
81+
handler, kwargs = url_map_adapter.match(
82+
path_info=parsed.path,
83+
query_args=parsed.query,
84+
)
85+
except RequestRedirect as redirect:
86+
return self.redirect(connection, redirect.new_url)
87+
except NotFound:
88+
return self.not_found(connection)
89+
connection.handler, connection.handler_kwargs = handler, kwargs
90+
return None
91+
92+
async def handler(self, connection: ServerConnection) -> None:
93+
"""Handle a connection."""
94+
return await connection.handler(connection, **connection.handler_kwargs)
95+
96+
def route(
97+
url_map: Map,
98+
*args: Any,
99+
server_name: str | None = None,
100+
ssl: ssl_module.SSLContext | Literal[True] | None = None,
101+
create_router: type[Router] | None = None,
102+
**kwargs: Any,
103+
) -> Awaitable[Server]:
104+
"""
105+
Create a WebSocket server dispatching connections to different handlers.
85106
86-
.. code-block:: console
107+
This feature requires the third-party library `werkzeug`_:
87108
88-
$ pip install werkzeug
109+
.. code-block:: console
89110
90-
.. _werkzeug: https://werkzeug.palletsprojects.com/
111+
$ pip install werkzeug
91112
92-
:func:`route` accepts the same arguments as
93-
:func:`~websockets.sync.server.serve`, except as described below.
113+
.. _werkzeug: https://werkzeug.palletsprojects.com/
94114
95-
The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns
96-
to connection handlers. In addition to the connection, handlers receive
97-
parameters captured in the URL as keyword arguments.
115+
:func:`route` accepts the same arguments as
116+
:func:`~websockets.sync.server.serve`, except as described below.
98117
99-
Here's an example::
118+
The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns
119+
to connection handlers. In addition to the connection, handlers receive
120+
parameters captured in the URL as keyword arguments.
100121
122+
Here's an example::
101123
102-
from websockets.asyncio.router import route
103-
from werkzeug.routing import Map, Rule
104124
105-
async def channel_handler(websocket, channel_id):
106-
...
125+
from websockets.asyncio.router import route
126+
from werkzeug.routing import Map, Rule
107127
108-
url_map = Map([
109-
Rule("/channel/<uuid:channel_id>", endpoint=channel_handler),
110-
...
111-
])
128+
async def channel_handler(websocket, channel_id):
129+
...
112130
113-
# set this future to exit the server
114-
stop = asyncio.get_running_loop().create_future()
131+
url_map = Map([
132+
Rule("/channel/<uuid:channel_id>", endpoint=channel_handler),
133+
...
134+
])
115135
116-
async with route(url_map, ...) as server:
117-
await stop
136+
# set this future to exit the server
137+
stop = asyncio.get_running_loop().create_future()
118138
139+
async with route(url_map, ...) as server:
140+
await stop
119141
120-
Refer to the documentation of :mod:`werkzeug.routing` for details.
121142
122-
If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map,
123-
when the server runs behind a reverse proxy that modifies the ``Host``
124-
header or terminates TLS, you need additional configuration:
143+
Refer to the documentation of :mod:`werkzeug.routing` for details.
125144
126-
* Set ``server_name`` to the name of the server as seen by clients. When not
127-
provided, websockets uses the value of the ``Host`` header.
145+
If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map,
146+
when the server runs behind a reverse proxy that modifies the ``Host``
147+
header or terminates TLS, you need additional configuration:
128148
129-
* Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling
130-
TLS. Under the hood, this bind the URL map with a ``url_scheme`` of
131-
``wss://`` instead of ``ws://``.
149+
* Set ``server_name`` to the name of the server as seen by clients. When not
150+
provided, websockets uses the value of the ``Host`` header.
132151
133-
There is no need to specify ``websocket=True`` in each rule. It is added
134-
automatically.
152+
* Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling
153+
TLS. Under the hood, this bind the URL map with a ``url_scheme`` of
154+
``wss://`` instead of ``ws://``.
135155
136-
Args:
137-
url_map: Mapping of URL patterns to connection handlers.
138-
server_name: Name of the server as seen by clients. If :obj:`None`,
139-
websockets uses the value of the ``Host`` header.
140-
ssl: Configuration for enabling TLS on the connection. Set it to
141-
:obj:`True` if a reverse proxy terminates TLS connections.
142-
create_router: Factory for the :class:`Router` dispatching requests to
143-
handlers. Set it to a wrapper or a subclass to customize routing.
156+
There is no need to specify ``websocket=True`` in each rule. It is added
157+
automatically.
144158
145-
"""
146-
url_scheme = "ws" if ssl is None else "wss"
147-
if ssl is not True and ssl is not None:
148-
kwargs["ssl"] = ssl
159+
Args:
160+
url_map: Mapping of URL patterns to connection handlers.
161+
server_name: Name of the server as seen by clients. If :obj:`None`,
162+
websockets uses the value of the ``Host`` header.
163+
ssl: Configuration for enabling TLS on the connection. Set it to
164+
:obj:`True` if a reverse proxy terminates TLS connections.
165+
create_router: Factory for the :class:`Router` dispatching requests to
166+
handlers. Set it to a wrapper or a subclass to customize routing.
149167
150-
if create_router is None:
151-
create_router = Router
168+
"""
169+
url_scheme = "ws" if ssl is None else "wss"
170+
if ssl is not True and ssl is not None:
171+
kwargs["ssl"] = ssl
152172

153-
router = create_router(url_map, server_name, url_scheme)
173+
if create_router is None:
174+
create_router = Router
154175

155-
_process_request: (
156-
Callable[
157-
[ServerConnection, Request],
158-
Awaitable[Response | None] | Response | None,
159-
]
160-
| None
161-
) = kwargs.pop("process_request", None)
162-
if _process_request is None:
163-
process_request: Callable[
164-
[ServerConnection, Request],
165-
Awaitable[Response | None] | Response | None,
166-
] = router.route_request
167-
else:
176+
router = create_router(url_map, server_name, url_scheme)
168177

169-
async def process_request(
170-
connection: ServerConnection, request: Request
171-
) -> Response | None:
172-
response = _process_request(connection, request)
173-
if isinstance(response, Awaitable):
174-
response = await response
175-
if response is not None:
176-
return response
177-
return router.route_request(connection, request)
178+
_process_request: (
179+
Callable[
180+
[ServerConnection, Request],
181+
Awaitable[Response | None] | Response | None,
182+
]
183+
| None
184+
) = kwargs.pop("process_request", None)
185+
if _process_request is None:
186+
process_request: Callable[
187+
[ServerConnection, Request],
188+
Awaitable[Response | None] | Response | None,
189+
] = router.route_request
190+
else:
178191

179-
return serve(router.handler, *args, process_request=process_request, **kwargs)
192+
async def process_request(
193+
connection: ServerConnection, request: Request
194+
) -> Response | None:
195+
response = _process_request(connection, request)
196+
if isinstance(response, Awaitable):
197+
response = await response
198+
if response is not None:
199+
return response
200+
return router.route_request(connection, request)
180201

202+
return serve(router.handler, *args, process_request=process_request, **kwargs)
181203

182-
def unix_route(
183-
url_map: Map,
184-
path: str | None = None,
185-
**kwargs: Any,
186-
) -> Awaitable[Server]:
187-
"""
188-
Create a WebSocket Unix server dispatching connections to different handlers.
204+
def unix_route(
205+
url_map: Map,
206+
path: str | None = None,
207+
**kwargs: Any,
208+
) -> Awaitable[Server]:
209+
"""
210+
Create a WebSocket Unix server dispatching connections to different handlers.
189211
190-
:func:`unix_route` combines the behaviors of :func:`route` and
191-
:func:`~websockets.asyncio.server.unix_serve`.
212+
:func:`unix_route` combines the behaviors of :func:`route` and
213+
:func:`~websockets.asyncio.server.unix_serve`.
192214
193-
Args:
194-
url_map: Mapping of URL patterns to connection handlers.
195-
path: File system path to the Unix socket.
215+
Args:
216+
url_map: Mapping of URL patterns to connection handlers.
217+
path: File system path to the Unix socket.
196218
197-
"""
198-
return route(url_map, unix=True, path=path, **kwargs)
219+
"""
220+
return route(url_map, unix=True, path=path, **kwargs)

0 commit comments

Comments
 (0)