From 1c8ee626b395e9e4d9f40cac8026ada4e4dbb9d8 Mon Sep 17 00:00:00 2001 From: Lev Blit Date: Mon, 13 Oct 2025 23:43:29 +0300 Subject: [PATCH 1/5] [simple-websocket]: add stubs for simple-websocket --- stubs/simple-websocket/METADATA.toml | 3 + .../simple_websocket/__init__.pyi | 3 + .../simple_websocket/aiows.pyi | 129 +++++++++++++++++ .../simple_websocket/asgi.pyi | 44 ++++++ .../simple_websocket/errors.pyi | 12 ++ .../simple-websocket/simple_websocket/ws.pyi | 132 ++++++++++++++++++ 6 files changed, 323 insertions(+) create mode 100644 stubs/simple-websocket/METADATA.toml create mode 100644 stubs/simple-websocket/simple_websocket/__init__.pyi create mode 100644 stubs/simple-websocket/simple_websocket/aiows.pyi create mode 100644 stubs/simple-websocket/simple_websocket/asgi.pyi create mode 100644 stubs/simple-websocket/simple_websocket/errors.pyi create mode 100644 stubs/simple-websocket/simple_websocket/ws.pyi diff --git a/stubs/simple-websocket/METADATA.toml b/stubs/simple-websocket/METADATA.toml new file mode 100644 index 000000000000..9b307e6bda7c --- /dev/null +++ b/stubs/simple-websocket/METADATA.toml @@ -0,0 +1,3 @@ +version = "1.1.*" +upstream_repository = "https://github.com/miguelgrinberg/simple-websocket" +requires = ["wsproto"] diff --git a/stubs/simple-websocket/simple_websocket/__init__.pyi b/stubs/simple-websocket/simple_websocket/__init__.pyi new file mode 100644 index 000000000000..d3da863383ca --- /dev/null +++ b/stubs/simple-websocket/simple_websocket/__init__.pyi @@ -0,0 +1,3 @@ +from .aiows import AioClient as AioClient, AioServer as AioServer +from .errors import ConnectionClosed as ConnectionClosed, ConnectionError as ConnectionError +from .ws import Client as Client, Server as Server diff --git a/stubs/simple-websocket/simple_websocket/aiows.pyi b/stubs/simple-websocket/simple_websocket/aiows.pyi new file mode 100644 index 000000000000..3e04d9eccaad --- /dev/null +++ b/stubs/simple-websocket/simple_websocket/aiows.pyi @@ -0,0 +1,129 @@ +import asyncio +import socket +from _typeshed import Incomplete, Unused +from _typeshed.wsgi import WSGIEnvironment +from collections.abc import Awaitable, Callable +from ssl import SSLContext +from typing import Any, Literal, TypedDict, type_check_only + +from wsproto import ConnectionType, WSConnection +from wsproto.events import Request +from wsproto.frame_protocol import CloseReason + +from .asgi import WebSocketASGI, _SocketDataBase, _SocketDataBytes, _SocketDataProtocol, _SocketDataStr + +class AioBase: + subprotocol: str | None + connection_type: ConnectionType + receive_bytes: int + ping_interval: float | None + max_message_size: int | None + pong_received: bool + input_buffer: list[bytes | str] + incoming_message: bytes | str | None + incoming_message_len: int + connected: bool + is_server: bool + close_reason: CloseReason + close_message: str + rsock: asyncio.StreamReader + wsock: asyncio.StreamWriter + event: asyncio.Event + ws: WSConnection | None + task: asyncio.Task[None] + def __init__( + self, + connection_type: ConnectionType | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ) -> None: ... + async def connect(self) -> None: ... + async def handshake(self) -> None: ... + async def send(self, data: bytes | Any) -> None: ... + async def receive(self, timeout: float | None = None): ... + async def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... + def choose_subprotocol(self, request: Request) -> str | None: ... + +@type_check_only +class _AioServerRequest(TypedDict): + # this is `aiohttp.web.Request` + aiohttp: Incomplete + sock: None + headers: None + +class AioServer(AioBase): + request: _AioServerRequest + headers: dict[str, Any] + subprotocols: list[str] + is_server: Literal[True] + mode: str + connected: bool + def __init__( + self, + request: _AioServerRequest, + subprotocols: list[str] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ) -> None: ... + @classmethod + async def accept( + cls, + # this is `aiohttp.web.Request` + aiohttp=None, + asgi: ( + tuple[ + WSGIEnvironment, + Callable[[], Awaitable[_SocketDataBytes | _SocketDataStr]], + Callable[[_SocketDataBase | _SocketDataProtocol | _SocketDataBytes | _SocketDataStr], Awaitable[None]], + ] + | None + ) = None, + sock: socket.socket | None = None, + headers: dict[str, Any] | None = None, + subprotocols: list[str] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ) -> WebSocketASGI | AioServer: ... + async def handshake(self) -> None: ... + def choose_subprotocol(self, request: Request) -> str | None: ... + +class AioClient(AioBase): + url: str + ssl_context: SSLContext | None + is_secure: bool + host: str + port: int + path: str + subprotocols: list[str] + extra_headeers: list[tuple[bytes, bytes]] + subprotocol: str | None + connected: bool + def __init__( + self, + url: str, + subprotocols: list[str] | None = None, + headers: dict[str, Any] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ssl_context: SSLContext | None = None, + ) -> None: ... + # the source code itself has this override + @classmethod + async def connect( # type: ignore[override] + cls, + url: str, + subprotocols: list[str] | None = None, + headers: dict[str, Any] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ssl_context: SSLContext | None = None, + thread_class: Unused = None, + event_class: Unused = None, + ) -> AioClient: ... + async def handshake(self) -> None: ... + async def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... diff --git a/stubs/simple-websocket/simple_websocket/asgi.pyi b/stubs/simple-websocket/simple_websocket/asgi.pyi new file mode 100644 index 000000000000..92e802188c5e --- /dev/null +++ b/stubs/simple-websocket/simple_websocket/asgi.pyi @@ -0,0 +1,44 @@ +from _typeshed.wsgi import WSGIEnvironment +from collections.abc import Awaitable, Callable +from typing import TypedDict, type_check_only + +@type_check_only +class _SocketDataBase(TypedDict): + type: str + +@type_check_only +class _SocketDataProtocol(_SocketDataBase): + subprotocol: str | None + +@type_check_only +class _SocketDataStr(_SocketDataBase): + text: str + +@type_check_only +class _SocketDataBytes(_SocketDataBase): + bytes: bytes + +class WebSocketASGI: + subprotocols: list[str] + subprotocol: str + connected: bool + # this is set in `close` to `False` + conncted: bool + def __init__( + self, + scope: WSGIEnvironment, + receive: Callable[[], Awaitable[_SocketDataBytes | _SocketDataStr]], + send: Callable[[_SocketDataBase | _SocketDataProtocol | _SocketDataBytes | _SocketDataStr], Awaitable[None]], + subprotocols: list[str] | None = None, + ) -> None: ... + @classmethod + async def accept( + cls, + scope: WSGIEnvironment, + receive: Callable[[], Awaitable[_SocketDataBytes | _SocketDataStr]], + send: Callable[[_SocketDataBase | _SocketDataProtocol | _SocketDataBytes | _SocketDataStr], Awaitable[None]], + subprotocols: list[str] | None = None, + ) -> WebSocketASGI: ... + async def receive(self) -> bytes | str: ... + async def send(self, data: bytes | str) -> None: ... + async def close(self) -> None: ... diff --git a/stubs/simple-websocket/simple_websocket/errors.pyi b/stubs/simple-websocket/simple_websocket/errors.pyi new file mode 100644 index 000000000000..88e25065121e --- /dev/null +++ b/stubs/simple-websocket/simple_websocket/errors.pyi @@ -0,0 +1,12 @@ +from wsproto.frame_protocol import CloseReason + +class SimpleWebsocketError(RuntimeError): ... + +class ConnectionError(SimpleWebsocketError): + status_code: int | None + def __init__(self, status_code: int | None = None) -> None: ... + +class ConnectionClosed(SimpleWebsocketError): + reason: CloseReason + message: str | None + def __init__(self, reason: CloseReason = ..., message: str | None = None) -> None: ... diff --git a/stubs/simple-websocket/simple_websocket/ws.pyi b/stubs/simple-websocket/simple_websocket/ws.pyi new file mode 100644 index 000000000000..75b3012e901d --- /dev/null +++ b/stubs/simple-websocket/simple_websocket/ws.pyi @@ -0,0 +1,132 @@ +import socket +import threading +from _typeshed import FileDescriptorLike +from _typeshed.wsgi import WSGIEnvironment +from collections.abc import Callable +from selectors import SelectorKey, _EventMask +from ssl import SSLContext +from typing import Any, Protocol, type_check_only + +from wsproto import ConnectionType, WSConnection +from wsproto.events import Request +from wsproto.frame_protocol import CloseReason + +@type_check_only +class _ThreadClassProtocol(Protocol): + name: str + def __init__(self, target: Callable[..., Any], *args: Any, **kwargs: Any) -> None: ... + def start(self) -> None: ... + +@type_check_only +class _EventClassProtocol(Protocol): + def clear(self) -> None: ... + def set(self) -> None: ... + def wait(self, timeout: float | None = None): ... + +@type_check_only +class _SelectorClassProtocol(Protocol): + def register(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... + def close(self) -> None: ... + +class Base: + subprotocol: str | None + sock: socket.socket | None + receive_bytes: int + ping_interval: float | None + max_message_size: int | None + pong_received: bool + input_buffer: list[bytes | str] + incoming_message: bytes | str | None + incoming_message_len: int + connected: bool + is_server: bool + close_reason: CloseReason + close_message: str | None + selector_class: type[_SelectorClassProtocol] + event: _EventClassProtocol | threading.Event + ws: WSConnection + thread: _ThreadClassProtocol | threading.Thread + def __init__( + self, + sock: socket.socket | None = None, + connection_type: ConnectionType | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + thread_class: type[_ThreadClassProtocol] | None = None, + event_class: type[_EventClassProtocol] | None = None, + selector_class: type[_SelectorClassProtocol] | None = None, + ) -> None: ... + def handshake(self) -> None: ... + def send(self, data: bytes | Any) -> None: ... + def receive(self, timeout: float | None = None) -> bytes | str | None: ... + def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... + def choose_subprotocol(self, request: Request) -> str | None: ... + +class Server(Base): + environ: WSGIEnvironment + subprotocols: list[str] + mode: str + connected: bool + def __init__( + self, + environ: WSGIEnvironment, + subprotocols: list[str] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + thread_class: type[_ThreadClassProtocol] | None = None, + event_class: type[_EventClassProtocol] | None = None, + selector_class: type[_SelectorClassProtocol] | None = None, + ) -> None: ... + @classmethod + def accept( + cls, + environ: WSGIEnvironment, + subprotocols: list[str] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + thread_class: type[_ThreadClassProtocol] | None = None, + event_class: type[_EventClassProtocol] | None = None, + selector_class: type[_SelectorClassProtocol] | None = None, + ) -> Server: ... + def handshake(self) -> None: ... + def choose_subprotocol(self, request: Request) -> str | None: ... + +class Client(Base): + host: str + port: int + path: str + subprotocols: list[str] + extra_headeers: list[tuple[bytes, bytes]] + subprotocol: str | None + connected: bool + def __init__( + self, + url: str, + subprotocols: list[str] | None = None, + headers: dict[bytes, bytes] | list[tuple[bytes, bytes]] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ssl_context: SSLContext | None = None, + thread_class: type[_ThreadClassProtocol] | None = None, + event_class: type[_EventClassProtocol] | None = None, + ) -> None: ... + @classmethod + def connect( + cls, + url: str, + subprotocols: list[str] | None = None, + headers: dict[bytes, bytes] | list[tuple[bytes, bytes]] | None = None, + receive_bytes: int = 4096, + ping_interval: float | None = None, + max_message_size: int | None = None, + ssl_context: SSLContext | None = None, + thread_class: type[_ThreadClassProtocol] | None = None, + event_class: type[_EventClassProtocol] | None = None, + ): ... + def handshake(self) -> None: ... + def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... From ca2013fd2926e115fc2666c2803ef203763956d5 Mon Sep 17 00:00:00 2001 From: Lev Blit Date: Mon, 13 Oct 2025 23:51:44 +0300 Subject: [PATCH 2/5] add missing return types --- stubs/simple-websocket/simple_websocket/aiows.pyi | 2 +- stubs/simple-websocket/simple_websocket/ws.pyi | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/simple-websocket/simple_websocket/aiows.pyi b/stubs/simple-websocket/simple_websocket/aiows.pyi index 3e04d9eccaad..b44ed38bcaff 100644 --- a/stubs/simple-websocket/simple_websocket/aiows.pyi +++ b/stubs/simple-websocket/simple_websocket/aiows.pyi @@ -41,7 +41,7 @@ class AioBase: async def connect(self) -> None: ... async def handshake(self) -> None: ... async def send(self, data: bytes | Any) -> None: ... - async def receive(self, timeout: float | None = None): ... + async def receive(self, timeout: float | None = None) -> bytes | str | Any: ... async def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... def choose_subprotocol(self, request: Request) -> str | None: ... diff --git a/stubs/simple-websocket/simple_websocket/ws.pyi b/stubs/simple-websocket/simple_websocket/ws.pyi index 75b3012e901d..80b0aaf3e7d3 100644 --- a/stubs/simple-websocket/simple_websocket/ws.pyi +++ b/stubs/simple-websocket/simple_websocket/ws.pyi @@ -21,7 +21,7 @@ class _ThreadClassProtocol(Protocol): class _EventClassProtocol(Protocol): def clear(self) -> None: ... def set(self) -> None: ... - def wait(self, timeout: float | None = None): ... + def wait(self, timeout: float | None = None) -> bool: ... @type_check_only class _SelectorClassProtocol(Protocol): @@ -127,6 +127,6 @@ class Client(Base): ssl_context: SSLContext | None = None, thread_class: type[_ThreadClassProtocol] | None = None, event_class: type[_EventClassProtocol] | None = None, - ): ... + ) -> Client: ... def handshake(self) -> None: ... def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... From 4edb60b3218fd3b8334ecc97ff6ffb08cace9c15 Mon Sep 17 00:00:00 2001 From: Lev Blit Date: Tue, 14 Oct 2025 00:05:17 +0300 Subject: [PATCH 3/5] re-add to stricter ignore list --- pyrightconfig.stricter.json | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrightconfig.stricter.json b/pyrightconfig.stricter.json index e1ac57dc5fcf..8d3409b139b4 100644 --- a/pyrightconfig.stricter.json +++ b/pyrightconfig.stricter.json @@ -88,6 +88,7 @@ "stubs/seaborn", "stubs/setuptools/setuptools", "stubs/shapely", + "stubs/simple-websocket", "stubs/tensorflow", "stubs/tqdm", "stubs/vobject", From 020792952d7a8a327c248e5be9a71443eec7cf36 Mon Sep 17 00:00:00 2001 From: Lev Blit Date: Sat, 1 Nov 2025 17:01:07 +0200 Subject: [PATCH 4/5] fix cr comments --- stubs/simple-websocket/simple_websocket/aiows.pyi | 2 +- stubs/simple-websocket/simple_websocket/ws.pyi | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stubs/simple-websocket/simple_websocket/aiows.pyi b/stubs/simple-websocket/simple_websocket/aiows.pyi index b44ed38bcaff..fa0cc6bee752 100644 --- a/stubs/simple-websocket/simple_websocket/aiows.pyi +++ b/stubs/simple-websocket/simple_websocket/aiows.pyi @@ -41,7 +41,7 @@ class AioBase: async def connect(self) -> None: ... async def handshake(self) -> None: ... async def send(self, data: bytes | Any) -> None: ... - async def receive(self, timeout: float | None = None) -> bytes | str | Any: ... + async def receive(self, timeout: float | None = None) -> bytes | str | None: ... async def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... def choose_subprotocol(self, request: Request) -> str | None: ... diff --git a/stubs/simple-websocket/simple_websocket/ws.pyi b/stubs/simple-websocket/simple_websocket/ws.pyi index 80b0aaf3e7d3..3c0f983d7d32 100644 --- a/stubs/simple-websocket/simple_websocket/ws.pyi +++ b/stubs/simple-websocket/simple_websocket/ws.pyi @@ -14,7 +14,7 @@ from wsproto.frame_protocol import CloseReason @type_check_only class _ThreadClassProtocol(Protocol): name: str - def __init__(self, target: Callable[..., Any], *args: Any, **kwargs: Any) -> None: ... + def __init__(self, target: Callable[..., Any]) -> None: ... def start(self) -> None: ... @type_check_only @@ -25,7 +25,9 @@ class _EventClassProtocol(Protocol): @type_check_only class _SelectorClassProtocol(Protocol): + # the signature of `register` here is the same as `selectors._BaseSelectorImpl` from the stdlib def register(self, fileobj: FileDescriptorLike, events: _EventMask, data: Any = None) -> SelectorKey: ... + # the signature of `select` here is the same as `selectors.DefaultSelector` from the stdlib def select(self, timeout: float | None = None) -> list[tuple[SelectorKey, _EventMask]]: ... def close(self) -> None: ... From 0c2afec948d27c864b1bf4aa80c92dfaf7fc1fad Mon Sep 17 00:00:00 2001 From: Lev Blit Date: Sat, 1 Nov 2025 17:18:45 +0200 Subject: [PATCH 5/5] add comments about Any --- stubs/simple-websocket/simple_websocket/aiows.pyi | 1 + stubs/simple-websocket/simple_websocket/ws.pyi | 2 ++ 2 files changed, 3 insertions(+) diff --git a/stubs/simple-websocket/simple_websocket/aiows.pyi b/stubs/simple-websocket/simple_websocket/aiows.pyi index fa0cc6bee752..066a243cb78e 100644 --- a/stubs/simple-websocket/simple_websocket/aiows.pyi +++ b/stubs/simple-websocket/simple_websocket/aiows.pyi @@ -40,6 +40,7 @@ class AioBase: ) -> None: ... async def connect(self) -> None: ... async def handshake(self) -> None: ... + # data can be antyhing. a special case is made for `bytes`, anything else is converted to `str`. async def send(self, data: bytes | Any) -> None: ... async def receive(self, timeout: float | None = None) -> bytes | str | None: ... async def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ... diff --git a/stubs/simple-websocket/simple_websocket/ws.pyi b/stubs/simple-websocket/simple_websocket/ws.pyi index 3c0f983d7d32..d469cd38d672 100644 --- a/stubs/simple-websocket/simple_websocket/ws.pyi +++ b/stubs/simple-websocket/simple_websocket/ws.pyi @@ -14,6 +14,7 @@ from wsproto.frame_protocol import CloseReason @type_check_only class _ThreadClassProtocol(Protocol): name: str + # this accepts any callable as the target, like `threading.Thread` def __init__(self, target: Callable[..., Any]) -> None: ... def start(self) -> None: ... @@ -61,6 +62,7 @@ class Base: selector_class: type[_SelectorClassProtocol] | None = None, ) -> None: ... def handshake(self) -> None: ... + # data can be antyhing. a special case is made for `bytes`, anything else is converted to `str`. def send(self, data: bytes | Any) -> None: ... def receive(self, timeout: float | None = None) -> bytes | str | None: ... def close(self, reason: CloseReason | None = None, message: str | None = None) -> None: ...