From f4c73b76c74044c53f700360792448ee0328983b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 26 Aug 2026 07:38:22 +0200 Subject: [PATCH 1/2] Accept pathlib.Path objects in path arguments. This was already working in the asyncio and trio implementations because we're relying on high-level helpers that already support such objects. Changes were only necessary for the threading implementation. --- docs/project/changelog.rst | 3 +++ docs/reference/types.rst | 2 ++ docs/spelling_wordlist.txt | 1 + src/websockets/asyncio/client.py | 4 ++-- src/websockets/asyncio/router.py | 5 +++-- src/websockets/asyncio/server.py | 4 ++-- src/websockets/sync/client.py | 12 ++++++------ src/websockets/sync/router.py | 5 +++-- src/websockets/sync/server.py | 7 ++++--- src/websockets/trio/client.py | 4 ++-- src/websockets/typing.py | 6 ++++-- tests/asyncio/test_client.py | 8 ++++++++ tests/asyncio/test_server.py | 8 ++++++++ tests/sync/test_client.py | 8 ++++++++ tests/sync/test_server.py | 8 ++++++++ tests/trio/test_client.py | 9 +++++++++ 16 files changed, 73 insertions(+), 21 deletions(-) diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index c3c77e70a..e80b1d27e 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -50,6 +50,9 @@ Improvements * Connections are now garbage collected immediately once closed. +* :func:`~sync.client.unix_connect` and :func:`~sync.server.unix_serve` now accept + path-like objects, such as :class:`pathlib.Path`, in the ``path`` argument. + .. _17.0.1: 17.0.1 diff --git a/docs/reference/types.rst b/docs/reference/types.rst index c26676804..ebb437620 100644 --- a/docs/reference/types.rst +++ b/docs/reference/types.rst @@ -15,6 +15,8 @@ Types .. autodata:: LoggerLike +.. autodata:: PathLike + .. autodata:: StatusLike .. autodata:: Origin diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 1d5e6e585..0e45a9f85 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -25,6 +25,7 @@ dev django Dockerfile dyno +filesystem formatter fractalideas github diff --git a/src/websockets/asyncio/client.py b/src/websockets/asyncio/client.py index 4c02f5964..b6426230f 100644 --- a/src/websockets/asyncio/client.py +++ b/src/websockets/asyncio/client.py @@ -27,7 +27,7 @@ from ..protocol import CONNECTING, Event from ..proxy import Proxy, get_proxy, parse_proxy, prepare_connect_request from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol +from ..typing import LoggerLike, Origin, PathLike, Subprotocol from ..uri import WebSocketURI, parse_uri from .connection import Connection @@ -630,7 +630,7 @@ async def __aiter__(self) -> AsyncIterator[ClientConnection]: def unix_connect( - path: str | None = None, + path: PathLike | None = None, uri: str | None = None, **kwargs: Any, ) -> connect: diff --git a/src/websockets/asyncio/router.py b/src/websockets/asyncio/router.py index 790c1ec46..e46111e53 100644 --- a/src/websockets/asyncio/router.py +++ b/src/websockets/asyncio/router.py @@ -6,6 +6,7 @@ from typing import Any, Awaitable, Callable, Literal from ..http11 import Request, Response +from ..typing import PathLike from .server import Server, ServerConnection, serve @@ -30,7 +31,7 @@ def route( def unix_route( url_map: Map, - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: raise ImportError("unix_route() requires werkzeug") @@ -155,7 +156,7 @@ async def process_request( def unix_route( url_map: Map, - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: """ diff --git a/src/websockets/asyncio/server.py b/src/websockets/asyncio/server.py index fced3e50e..7a168d04a 100644 --- a/src/websockets/asyncio/server.py +++ b/src/websockets/asyncio/server.py @@ -22,7 +22,7 @@ from ..http11 import SERVER, Request, Response from ..protocol import CONNECTING, OPEN, Event from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol +from ..typing import LoggerLike, Origin, PathLike, StatusLike, Subprotocol from ..utils import get_socket_name from .connection import Connection, broadcast @@ -769,7 +769,7 @@ async def protocol_handler(connection: ServerConnection) -> None: def unix_serve( handler: Callable[[ServerConnection], Awaitable[None]], - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: """ diff --git a/src/websockets/sync/client.py b/src/websockets/sync/client.py index 72f57f778..d3d8a0645 100644 --- a/src/websockets/sync/client.py +++ b/src/websockets/sync/client.py @@ -29,7 +29,7 @@ from ..protocol import CONNECTING, Event from ..proxy import Proxy, get_proxy, parse_proxy, prepare_connect_request from ..streams import StreamReader -from ..typing import BytesLike, LoggerLike, Origin, Subprotocol +from ..typing import BytesLike, LoggerLike, Origin, PathLike, Subprotocol from ..uri import WebSocketURI, parse_uri from .connection import Connection from .utils import Deadline @@ -308,7 +308,7 @@ def open_socket(self, deadline: Deadline) -> socket.socket: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: sock.settimeout(deadline.timeout()) - sock.connect(kwargs.pop("path")) + sock.connect(os.fspath(kwargs.pop("path"))) except Exception: sock.close() raise @@ -821,7 +821,7 @@ def connect( def unix_reconnect( - path: str | None = None, + path: PathLike | None = None, uri: str | None = None, **kwargs: Any, ) -> reconnect: @@ -849,7 +849,7 @@ def unix_reconnect( @overload def unix_connect( - path: str | None = ..., + path: PathLike | None = ..., uri: str | None = ..., *, legacy: Literal[True] | None = ..., @@ -859,7 +859,7 @@ def unix_connect( @overload def unix_connect( - path: str | None = ..., + path: PathLike | None = ..., uri: str | None = ..., *, legacy: Literal[False], @@ -868,7 +868,7 @@ def unix_connect( def unix_connect( - path: str | None = None, + path: PathLike | None = None, uri: str | None = None, *, legacy: bool | None = None, diff --git a/src/websockets/sync/router.py b/src/websockets/sync/router.py index 1c35e8aae..3cd7464b2 100644 --- a/src/websockets/sync/router.py +++ b/src/websockets/sync/router.py @@ -6,6 +6,7 @@ from typing import Any, Callable, Literal from ..http11 import Request, Response +from ..typing import PathLike from .server import Server, ServerConnection, serve @@ -30,7 +31,7 @@ def route( def unix_route( url_map: Map, - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: raise ImportError("unix_route() requires werkzeug") @@ -141,7 +142,7 @@ def process_request( def unix_route( url_map: Map, - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: """ diff --git a/src/websockets/sync/server.py b/src/websockets/sync/server.py index 97a8d9d46..6a00bdaa0 100644 --- a/src/websockets/sync/server.py +++ b/src/websockets/sync/server.py @@ -4,6 +4,7 @@ import hmac import http import logging +import os import re import selectors import socket @@ -28,7 +29,7 @@ from ..http11 import SERVER, Request, Response from ..protocol import CONNECTING, OPEN, Event from ..server import ServerProtocol -from ..typing import LoggerLike, Origin, StatusLike, Subprotocol +from ..typing import LoggerLike, Origin, PathLike, StatusLike, Subprotocol from ..utils import get_socket_name from .connection import Connection, broadcast from .utils import Deadline @@ -645,7 +646,7 @@ def handler(websocket): if path is None: raise ValueError("missing path argument") kwargs.setdefault("family", socket.AF_UNIX) - sock = socket.create_server(path, **kwargs) + sock = socket.create_server(os.fspath(path), **kwargs) else: sock = socket.create_server((host, port), **kwargs) else: @@ -793,7 +794,7 @@ def protocol_select_subprotocol( def unix_serve( handler: Callable[[ServerConnection], None], - path: str | None = None, + path: PathLike | None = None, **kwargs: Any, ) -> Server: """ diff --git a/src/websockets/trio/client.py b/src/websockets/trio/client.py index 6dc198a9a..2af6785ba 100644 --- a/src/websockets/trio/client.py +++ b/src/websockets/trio/client.py @@ -27,7 +27,7 @@ from ..protocol import CONNECTING, Event from ..proxy import Proxy, get_proxy, parse_proxy, prepare_connect_request from ..streams import StreamReader -from ..typing import LoggerLike, Origin, Subprotocol +from ..typing import LoggerLike, Origin, PathLike, Subprotocol from ..uri import WebSocketURI, parse_uri from .connection import Connection from .utils import race_events @@ -640,7 +640,7 @@ async def __aiter__(self) -> AsyncIterator[ClientConnection]: def unix_connect( - path: str | None = None, + path: PathLike | None = None, uri: str | None = None, **kwargs: Any, ) -> connect: diff --git a/src/websockets/typing.py b/src/websockets/typing.py index 57e5d8683..07b675b17 100644 --- a/src/websockets/typing.py +++ b/src/websockets/typing.py @@ -2,6 +2,7 @@ import http import logging +import os from typing import Any, NewType, Sequence @@ -36,10 +37,11 @@ LoggerLike = logging.Logger | logging.LoggerAdapter[Any] """Types accepted where a :class:`~logging.Logger` is expected.""" +PathLike = str | bytes | os.PathLike[str] | os.PathLike[bytes] +"""Types accepted where a filesystem path is expected.""" StatusLike = http.HTTPStatus | int -""" -Types accepted where an :class:`~http.HTTPStatus` is expected.""" +"""Types accepted where an :class:`~http.HTTPStatus` is expected.""" Origin = NewType("Origin", str) diff --git a/tests/asyncio/test_client.py b/tests/asyncio/test_client.py index 6e6b62436..f8f563084 100644 --- a/tests/asyncio/test_client.py +++ b/tests/asyncio/test_client.py @@ -3,6 +3,7 @@ import http import logging import os +import pathlib import socket import ssl import sys @@ -993,6 +994,13 @@ async def test_set_server_hostname(self): ssl_object = client.transport.get_extra_info("ssl_object") self.assertEqual(ssl_object.server_hostname, "overridden") + async def test_pathlib_path(self): + """Client accepts a pathlib.Path object as the path argument.""" + with temp_unix_socket_path() as path: + async with unix_serve(handler, path): + async with unix_connect(pathlib.Path(path)) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + async def test_non_existing_path(self): """Client attempts to connect to a non-existing Unix socket path.""" with temp_unix_socket_path() as path: diff --git a/tests/asyncio/test_server.py b/tests/asyncio/test_server.py index a687337f8..6487cabde 100644 --- a/tests/asyncio/test_server.py +++ b/tests/asyncio/test_server.py @@ -3,6 +3,7 @@ import hmac import http import logging +import pathlib import socket import unittest @@ -742,6 +743,13 @@ async def test_connection(self): async with unix_connect(path) as client: await self.assertEval(client, "ws.protocol.state.name", "OPEN") + async def test_pathlib_path(self): + """Server accepts a pathlib.Path object as the path argument.""" + with temp_unix_socket_path() as path: + async with unix_serve(handler, pathlib.Path(path)): + async with unix_connect(path) as client: + await self.assertEval(client, "ws.protocol.state.name", "OPEN") + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") class SecureUnixServerTests(EvalShellMixin, unittest.IsolatedAsyncioTestCase): diff --git a/tests/sync/test_client.py b/tests/sync/test_client.py index 50d81e70d..e812fcc47 100644 --- a/tests/sync/test_client.py +++ b/tests/sync/test_client.py @@ -2,6 +2,7 @@ import http import logging import os +import pathlib import socket import socketserver import ssl @@ -1033,6 +1034,13 @@ def test_set_server_hostname(self): ) as client: self.assertEqual(client.socket.server_hostname, "overridden") + def test_pathlib_path(self): + """Client accepts a pathlib.Path object as the path argument.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + with unix_connect(pathlib.Path(path)) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + def test_non_existing_path(self): """Client attempts to connect to a non-existing Unix socket path.""" with temp_unix_socket_path() as path: diff --git a/tests/sync/test_server.py b/tests/sync/test_server.py index 92280ac3f..f241cc741 100644 --- a/tests/sync/test_server.py +++ b/tests/sync/test_server.py @@ -2,6 +2,7 @@ import hmac import http import logging +import pathlib import socket import threading import time @@ -524,6 +525,13 @@ def test_connection(self): with unix_connect(path) as client: self.assertEval(client, "ws.protocol.state.name", "OPEN") + def test_pathlib_path(self): + """Server accepts a pathlib.Path object as the path argument.""" + with temp_unix_socket_path() as path: + with run_unix_server(pathlib.Path(path)): + with unix_connect(path) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") class SecureUnixServerTests(EvalShellMixin, unittest.TestCase): diff --git a/tests/trio/test_client.py b/tests/trio/test_client.py index f43ac553e..fb08a097c 100644 --- a/tests/trio/test_client.py +++ b/tests/trio/test_client.py @@ -2,6 +2,7 @@ import http import logging import os +import pathlib import socket import ssl import sys @@ -978,6 +979,14 @@ async def test_set_server_hostname(self): ssl_object = client.stream._ssl_object self.assertEqual(ssl_object.server_hostname, "overridden") + async def test_pathlib_path(self): + """Client accepts a pathlib.Path object as the path argument.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + async with unix_connect(pathlib.Path(path)) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + self.assertEqual(client.protocol.state.name, "CLOSED") + async def test_non_existing_path(self): """Client attempts to connect to a non-existing Unix socket path.""" with temp_unix_socket_path() as path: From caf68ab866350ab43305bc0a2ce28e4c845b90ab Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 26 Aug 2026 07:55:42 +0200 Subject: [PATCH 2/2] Minor whitespace normalization. --- src/websockets/sync/client.py | 1 + src/websockets/trio/client.py | 1 + tests/trio/test_client.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/websockets/sync/client.py b/src/websockets/sync/client.py index d3d8a0645..95fe53ab0 100644 --- a/src/websockets/sync/client.py +++ b/src/websockets/sync/client.py @@ -463,6 +463,7 @@ def process_redirect(self, exc: Exception) -> Exception | str: f"cannot follow cross-origin redirect to {new_uri} " f"with a Unix socket" ) + # Cross-origin redirects when host and port are overridden are ill-defined. if self.open_socket_kwargs.get("address") is not None: return ValueError( diff --git a/src/websockets/trio/client.py b/src/websockets/trio/client.py index 2af6785ba..069fe4b22 100644 --- a/src/websockets/trio/client.py +++ b/src/websockets/trio/client.py @@ -482,6 +482,7 @@ def process_redirect(self, exc: Exception) -> Exception | str: f"cannot follow cross-origin redirect to {new_uri} " f"with a Unix socket" ) + # Cross-origin redirects when host and port are overridden are ill-defined. if ( self.open_tcp_stream_kwargs.get("host") is not None diff --git a/tests/trio/test_client.py b/tests/trio/test_client.py index fb08a097c..8d9d37743 100644 --- a/tests/trio/test_client.py +++ b/tests/trio/test_client.py @@ -985,7 +985,6 @@ async def test_pathlib_path(self): with run_unix_server(path): async with unix_connect(pathlib.Path(path)) as client: self.assertEqual(client.protocol.state.name, "OPEN") - self.assertEqual(client.protocol.state.name, "CLOSED") async def test_non_existing_path(self): """Client attempts to connect to a non-existing Unix socket path."""