From a3ef3ae74f315bd10519909ac1ce1ed1cd9d0d43 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sun, 4 May 2025 00:11:54 +0700 Subject: [PATCH 01/14] Django channels stubs --- pyrightconfig.stricter.json | 1 + stubs/channels/METADATA.toml | 8 ++ stubs/channels/channels/__init__.pyi | 2 + stubs/channels/channels/apps.pyi | 6 ++ stubs/channels/channels/auth.pyi | 32 +++++++ stubs/channels/channels/consumer.pyi | 54 +++++++++++ stubs/channels/channels/db.pyi | 15 +++ stubs/channels/channels/exceptions.pyi | 8 ++ stubs/channels/channels/generic/__init__.pyi | 0 stubs/channels/channels/generic/http.pyi | 19 ++++ stubs/channels/channels/generic/websocket.pyi | 64 +++++++++++++ stubs/channels/channels/layers.pyi | 96 +++++++++++++++++++ .../channels/channels/management/__init__.pyi | 0 .../channels/management/commands/__init__.pyi | 0 .../management/commands/runworker.pyi | 20 ++++ stubs/channels/channels/middleware.pyi | 12 +++ stubs/channels/channels/routing.pyi | 31 ++++++ stubs/channels/channels/security/__init__.pyi | 0 .../channels/channels/security/websocket.pyi | 25 +++++ stubs/channels/channels/sessions.pyi | 53 ++++++++++ stubs/channels/channels/testing/__init__.pyi | 6 ++ .../channels/channels/testing/application.pyi | 12 +++ stubs/channels/channels/testing/http.pyi | 39 ++++++++ stubs/channels/channels/testing/live.pyi | 34 +++++++ stubs/channels/channels/testing/websocket.pyi | 50 ++++++++++ stubs/channels/channels/utils.pyi | 15 +++ stubs/channels/channels/worker.pyi | 13 +++ 27 files changed, 615 insertions(+) create mode 100644 stubs/channels/METADATA.toml create mode 100644 stubs/channels/channels/__init__.pyi create mode 100644 stubs/channels/channels/apps.pyi create mode 100644 stubs/channels/channels/auth.pyi create mode 100644 stubs/channels/channels/consumer.pyi create mode 100644 stubs/channels/channels/db.pyi create mode 100644 stubs/channels/channels/exceptions.pyi create mode 100644 stubs/channels/channels/generic/__init__.pyi create mode 100644 stubs/channels/channels/generic/http.pyi create mode 100644 stubs/channels/channels/generic/websocket.pyi create mode 100644 stubs/channels/channels/layers.pyi create mode 100644 stubs/channels/channels/management/__init__.pyi create mode 100644 stubs/channels/channels/management/commands/__init__.pyi create mode 100644 stubs/channels/channels/management/commands/runworker.pyi create mode 100644 stubs/channels/channels/middleware.pyi create mode 100644 stubs/channels/channels/routing.pyi create mode 100644 stubs/channels/channels/security/__init__.pyi create mode 100644 stubs/channels/channels/security/websocket.pyi create mode 100644 stubs/channels/channels/sessions.pyi create mode 100644 stubs/channels/channels/testing/__init__.pyi create mode 100644 stubs/channels/channels/testing/application.pyi create mode 100644 stubs/channels/channels/testing/http.pyi create mode 100644 stubs/channels/channels/testing/live.pyi create mode 100644 stubs/channels/channels/testing/websocket.pyi create mode 100644 stubs/channels/channels/utils.pyi create mode 100644 stubs/channels/channels/worker.pyi diff --git a/pyrightconfig.stricter.json b/pyrightconfig.stricter.json index 9acd48158ca0..f08f827fff3a 100644 --- a/pyrightconfig.stricter.json +++ b/pyrightconfig.stricter.json @@ -31,6 +31,7 @@ "stubs/boltons", "stubs/braintree", "stubs/cffi", + "stubs/channels", "stubs/dateparser", "stubs/defusedxml", "stubs/docker", diff --git a/stubs/channels/METADATA.toml b/stubs/channels/METADATA.toml new file mode 100644 index 000000000000..7d980d0a436f --- /dev/null +++ b/stubs/channels/METADATA.toml @@ -0,0 +1,8 @@ +version = "4.*" +upstream_repository = "https://github.com/django/channels" +requires = ["django-stubs", "asgiref"] +requires_python = ">=3.10" + +[tool.stubtest] +skip = true # due to the need of django mypy plugin config, it should be skipped. +stubtest_requirements = ["daphne"] diff --git a/stubs/channels/channels/__init__.pyi b/stubs/channels/channels/__init__.pyi new file mode 100644 index 000000000000..ae22f453163e --- /dev/null +++ b/stubs/channels/channels/__init__.pyi @@ -0,0 +1,2 @@ +__version__: str +DEFAULT_CHANNEL_LAYER: str diff --git a/stubs/channels/channels/apps.pyi b/stubs/channels/channels/apps.pyi new file mode 100644 index 000000000000..9d479a2d49e5 --- /dev/null +++ b/stubs/channels/channels/apps.pyi @@ -0,0 +1,6 @@ +from django.apps import AppConfig +from django.utils.functional import _StrOrPromise + +class ChannelsConfig(AppConfig): + name: str = ... + verbose_name: _StrOrPromise = ... diff --git a/stubs/channels/channels/auth.pyi b/stubs/channels/channels/auth.pyi new file mode 100644 index 000000000000..5f3b7b5e20fe --- /dev/null +++ b/stubs/channels/channels/auth.pyi @@ -0,0 +1,32 @@ +from typing import Any + +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable +from channels.middleware import BaseMiddleware +from django.contrib.auth.backends import BaseBackend +from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.models import AnonymousUser +from django.utils.functional import LazyObject + +from .consumer import _ChannelScope, _LazySession +from .db import database_sync_to_async +from .utils import _ChannelApplication + +@database_sync_to_async +def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ... +@database_sync_to_async +def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = ...) -> None: ... +@database_sync_to_async +def logout(scope: _ChannelScope) -> None: ... +def _get_user_session_key(session: _LazySession) -> Any: ... + +class UserLazyObject(AbstractBaseUser, LazyObject): + def _setup(self) -> None: ... + +class AuthMiddleware(BaseMiddleware): + def populate_scope(self, scope: _ChannelScope) -> None: ... + async def resolve_scope(self, scope: _ChannelScope) -> None: ... + async def __call__( + self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> _ChannelApplication: ... + +def AuthMiddlewareStack(inner: _ChannelApplication) -> _ChannelApplication: ... diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi new file mode 100644 index 000000000000..bb7c8e332cee --- /dev/null +++ b/stubs/channels/channels/consumer.pyi @@ -0,0 +1,54 @@ +from collections.abc import Awaitable +from typing import Any, ClassVar, Protocol + +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WebSocketScope +from channels.auth import UserLazyObject +from channels.db import database_sync_to_async +from django.contrib.sessions.backends.base import SessionBase +from django.utils.functional import LazyObject + +class _LazySession(SessionBase, LazyObject): # type: ignore[misc] + _wrapped: SessionBase + +# Base ASGI Scope definition +class _ChannelScope(WebSocketScope, total=False): + # Channel specific + channel: str + url_route: dict[str, Any] + path_remaining: str + + # Auth specific + cookies: dict[str, str] + session: _LazySession + user: UserLazyObject | None + +def get_handler_name(message: dict[str, Any]) -> str: ... + +class _ASGIApplicationProtocol(Protocol): + consumer_class: Any + consumer_initkwargs: dict[str, Any] + + def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Awaitable[None]: ... + +class AsyncConsumer: + _sync: ClassVar[bool] = ... + channel_layer_alias: ClassVar[str] = ... + + scope: _ChannelScope + channel_layer: Any + channel_name: str + channel_receive: ASGIReceiveCallable + base_send: ASGISendCallable + + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def dispatch(self, message: dict[str, Any]) -> None: ... + async def send(self, message: dict[str, Any]) -> None: ... + @classmethod + def as_asgi(cls, **initkwargs: Any) -> _ASGIApplicationProtocol: ... + +class SyncConsumer(AsyncConsumer): + _sync: ClassVar[bool] = ... + + @database_sync_to_async + def dispatch(self, message: dict[str, Any]) -> None: ... # type: ignore[override] + def send(self, message: dict[str, Any]) -> None: ... # type: ignore[override] diff --git a/stubs/channels/channels/db.pyi b/stubs/channels/channels/db.pyi new file mode 100644 index 000000000000..98c68d1e21db --- /dev/null +++ b/stubs/channels/channels/db.pyi @@ -0,0 +1,15 @@ +from asyncio import BaseEventLoop +from collections.abc import Callable, Coroutine +from typing import Any, TypeVar +from typing_extensions import ParamSpec + +from asgiref.sync import SyncToAsync + +_P = ParamSpec("_P") +_R = TypeVar("_R") + +class DatabaseSyncToAsync(SyncToAsync[_P, _R]): + def thread_handler(self, loop: BaseEventLoop, *args: Any, **kwargs: Any) -> Any: ... + +def database_sync_to_async(func: Callable[_P, _R]) -> Callable[_P, Coroutine[Any, Any, _R]]: ... +async def aclose_old_connections() -> None: ... diff --git a/stubs/channels/channels/exceptions.pyi b/stubs/channels/channels/exceptions.pyi new file mode 100644 index 000000000000..eaba1dfaee14 --- /dev/null +++ b/stubs/channels/channels/exceptions.pyi @@ -0,0 +1,8 @@ +class RequestAborted(Exception): ... +class RequestTimeout(RequestAborted): ... +class InvalidChannelLayerError(ValueError): ... +class AcceptConnection(Exception): ... +class DenyConnection(Exception): ... +class ChannelFull(Exception): ... +class MessageTooLarge(Exception): ... +class StopConsumer(Exception): ... diff --git a/stubs/channels/channels/generic/__init__.pyi b/stubs/channels/channels/generic/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stubs/channels/channels/generic/http.pyi b/stubs/channels/channels/generic/http.pyi new file mode 100644 index 000000000000..85d83a7fd208 --- /dev/null +++ b/stubs/channels/channels/generic/http.pyi @@ -0,0 +1,19 @@ +from collections.abc import Iterable +from typing import Any + +from asgiref.typing import HTTPDisconnectEvent, HTTPRequestEvent, HTTPScope +from channels.consumer import AsyncConsumer + +class AsyncHttpConsumer(AsyncConsumer): + body: list[bytes] + scope: HTTPScope # type: ignore[assignment] + + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + async def send_headers(self, *, status: int = ..., headers: Iterable[tuple[bytes, bytes]] | None = ...) -> None: ... + async def send_body(self, body: bytes, *, more_body: bool = ...) -> None: ... + async def send_response(self, status: int, body: bytes, **kwargs: Any) -> None: ... + async def handle(self, body: bytes) -> None: ... + async def disconnect(self) -> None: ... + async def http_request(self, message: HTTPRequestEvent) -> None: ... + async def http_disconnect(self, message: HTTPDisconnectEvent) -> None: ... + async def send(self, message: dict[str, Any]) -> None: ... # type: ignore[override] diff --git a/stubs/channels/channels/generic/websocket.pyi b/stubs/channels/channels/generic/websocket.pyi new file mode 100644 index 000000000000..aa1a54443560 --- /dev/null +++ b/stubs/channels/channels/generic/websocket.pyi @@ -0,0 +1,64 @@ +from typing import Any + +from asgiref.typing import WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveEvent +from channels.consumer import AsyncConsumer, SyncConsumer, _ChannelScope + +class WebsocketConsumer(SyncConsumer): + groups: list[str] | None + scope: _ChannelScope + channel_name: str + channel_layer: Any + channel_receive: Any + base_send: Any + + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... + def connect(self) -> None: ... + def accept(self, subprotocol: str | None = ..., headers: list[tuple[str, str]] | None = ...) -> None: ... + def websocket_receive(self, message: WebSocketReceiveEvent) -> None: ... + def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + def send( # type: ignore[override] + self, text_data: str | None = ..., bytes_data: bytes | None = ..., close: bool = ... + ) -> None: ... + def close(self, code: int | bool | None = ..., reason: str | None = ...) -> None: ... + def websocket_disconnect(self, message: WebSocketDisconnectEvent) -> None: ... + def disconnect(self, code: int) -> None: ... + +class JsonWebsocketConsumer(WebsocketConsumer): + def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ..., **kwargs: Any) -> None: ... + def receive_json(self, content: Any, **kwargs: Any) -> None: ... + def send_json(self, content: Any, close: bool = ...) -> None: ... + @classmethod + def decode_json(cls, text_data: str) -> Any: ... + @classmethod + def encode_json(cls, content: Any) -> str: ... + +class AsyncWebsocketConsumer(AsyncConsumer): + groups: list[str] | None + scope: _ChannelScope + channel_name: str + channel_layer: Any + channel_receive: Any + base_send: Any + + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + async def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... + async def connect(self) -> None: ... + async def accept(self, subprotocol: str | None = ..., headers: list[tuple[str, str]] | None = ...) -> None: ... + async def websocket_receive(self, message: WebSocketReceiveEvent) -> None: ... + async def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + async def send( # type: ignore[override] + self, text_data: str | None = ..., bytes_data: bytes | None = ..., close: bool = ... + ) -> None: ... + async def close(self, code: int | bool | None = ..., reason: str | None = ...) -> None: ... + async def websocket_disconnect(self, message: WebSocketDisconnectEvent) -> None: ... + async def disconnect(self, code: int) -> None: ... + +class AsyncJsonWebsocketConsumer(AsyncWebsocketConsumer): + async def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ..., **kwargs: Any) -> None: ... + async def receive_json(self, content: Any, **kwargs: Any) -> None: ... + async def send_json(self, content: Any, close: bool = ...) -> None: ... + @classmethod + async def decode_json(cls, text_data: str) -> Any: ... + @classmethod + async def encode_json(cls, content: Any) -> str: ... diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi new file mode 100644 index 000000000000..46c90bbd6821 --- /dev/null +++ b/stubs/channels/channels/layers.pyi @@ -0,0 +1,96 @@ +import asyncio +from re import Pattern +from typing import Any, TypeAlias, overload +from typing_extensions import deprecated + +class ChannelLayerManager: + backends: dict[str, BaseChannelLayer] + + def __init__(self) -> None: ... + def _reset_backends(self, setting: str, **kwargs: Any) -> None: ... + @property + def configs(self) -> dict[str, Any]: ... + def make_backend(self, name: str) -> BaseChannelLayer: ... + def make_test_backend(self, name: str) -> Any: ... + def _make_backend(self, name: str, config: dict[str, Any]) -> BaseChannelLayer: ... + def __getitem__(self, key: str) -> BaseChannelLayer: ... + def __contains__(self, key: str) -> bool: ... + def set(self, key: str, layer: BaseChannelLayer) -> BaseChannelLayer | None: ... + +_ChannelCapacityPattern: TypeAlias = Pattern[str] | str +_ChannelCapacityDict: TypeAlias = dict[_ChannelCapacityPattern, int] +_CompiledChannelCapacity: TypeAlias = list[tuple[Pattern[str], int]] + +class BaseChannelLayer: + MAX_NAME_LENGTH: int = ... + expiry: int + capacity: int + channel_capacity: _ChannelCapacityDict + channel_name_regex: Pattern[str] + group_name_regex: Pattern[str] + invalid_name_error: str + + def __init__(self, expiry: int = ..., capacity: int = ..., channel_capacity: _ChannelCapacityDict | None = ...) -> None: ... + def compile_capacities(self, channel_capacity: _ChannelCapacityDict) -> _CompiledChannelCapacity: ... + def get_capacity(self, channel: str) -> int: ... + @overload + def match_type_and_length(self, name: str) -> bool: ... + @overload + def match_type_and_length(self, name: Any) -> bool: ... + @overload + def require_valid_channel_name(self, name: str, receive: bool = ...) -> bool: ... + @overload + def require_valid_channel_name(self, name: Any, receive: bool = ...) -> bool: ... + @overload + def require_valid_group_name(self, name: str) -> bool: ... + @overload + def require_valid_group_name(self, name: Any) -> bool: ... + @overload + def valid_channel_names(self, names: list[str], receive: bool = ...) -> bool: ... + @overload + def valid_channel_names(self, names: list[Any], receive: bool = ...) -> bool: ... + def non_local_name(self, name: str) -> str: ... + async def send(self, channel: str, message: dict[str, Any]) -> None: ... + async def receive(self, channel: str) -> dict[str, Any]: ... + async def new_channel(self) -> str: ... + async def flush(self) -> None: ... + async def group_add(self, group: str, channel: str) -> None: ... + async def group_discard(self, group: str, channel: str) -> None: ... + async def group_send(self, group: str, message: dict[str, Any]) -> None: ... + @deprecated("Use require_valid_channel_name instead.") + def valid_channel_name(self, channel_name: str, receive: bool = ...) -> bool: ... + @deprecated("Use require_valid_group_name instead.") + def valid_group_name(self, group_name: str) -> bool: ... + +_InMemoryQueueData: TypeAlias = tuple[float, dict[str, Any]] + +class InMemoryChannelLayer(BaseChannelLayer): + channels: dict[str, asyncio.Queue[_InMemoryQueueData]] + groups: dict[str, dict[str, float]] + group_expiry: int + + def __init__( + self, + expiry: int = ..., + group_expiry: int = ..., + capacity: int = ..., + channel_capacity: _ChannelCapacityDict | None = ..., + **kwargs: Any, + ) -> None: ... + + extensions: list[str] + + async def send(self, channel: str, message: dict[str, Any]) -> None: ... + async def receive(self, channel: str) -> dict[str, Any]: ... + async def new_channel(self, prefix: str = ...) -> str: ... + def _clean_expired(self) -> None: ... + async def flush(self) -> None: ... + async def close(self) -> None: ... + def _remove_from_groups(self, channel: str) -> None: ... + async def group_add(self, group: str, channel: str) -> None: ... + async def group_discard(self, group: str, channel: str) -> None: ... + async def group_send(self, group: str, message: dict[str, Any]) -> None: ... + +def get_channel_layer(alias: str = ...) -> BaseChannelLayer | None: ... + +channel_layers: ChannelLayerManager diff --git a/stubs/channels/channels/management/__init__.pyi b/stubs/channels/channels/management/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stubs/channels/channels/management/commands/__init__.pyi b/stubs/channels/channels/management/commands/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stubs/channels/channels/management/commands/runworker.pyi b/stubs/channels/channels/management/commands/runworker.pyi new file mode 100644 index 000000000000..1ea4a1e6d4f8 --- /dev/null +++ b/stubs/channels/channels/management/commands/runworker.pyi @@ -0,0 +1,20 @@ +import logging +from argparse import ArgumentParser +from typing import Any + +from channels.layers import BaseChannelLayer +from channels.worker import Worker +from django.core.management.base import BaseCommand + +logger: logging.Logger + +class Command(BaseCommand): + leave_locale_alone: bool = ... + worker_class: type[Worker] = ... + verbosity: int + channel_layer: BaseChannelLayer + + def add_arguments(self, parser: ArgumentParser) -> None: ... + def handle( + self, *args: Any, application_path: str | None = ..., channels: list[str] | None = ..., layer: str = ..., **options: Any + ) -> None: ... diff --git a/stubs/channels/channels/middleware.pyi b/stubs/channels/channels/middleware.pyi new file mode 100644 index 000000000000..339ae9218244 --- /dev/null +++ b/stubs/channels/channels/middleware.pyi @@ -0,0 +1,12 @@ +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable + +from .consumer import _ChannelScope +from .utils import _ChannelApplication + +class BaseMiddleware: + inner: _ChannelApplication + + def __init__(self, inner: _ChannelApplication) -> None: ... + async def __call__( + self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> _ChannelApplication: ... diff --git a/stubs/channels/channels/routing.pyi b/stubs/channels/channels/routing.pyi new file mode 100644 index 000000000000..ac98a8e4d7fd --- /dev/null +++ b/stubs/channels/channels/routing.pyi @@ -0,0 +1,31 @@ +from typing import Any + +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable +from django.urls.resolvers import URLPattern + +from .consumer import _ASGIApplicationProtocol, _ChannelScope +from .utils import _ChannelApplication + +def get_default_application() -> ProtocolTypeRouter: ... + +class ProtocolTypeRouter: + application_mapping: dict[str, _ChannelApplication] + + def __init__(self, application_mapping: dict[str, Any]) -> None: ... + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + +class _ExtendedURLPattern(URLPattern): + callback: _ASGIApplicationProtocol | URLRouter + +class URLRouter: + _path_routing: bool = ... + routes: list[_ExtendedURLPattern | URLRouter] + + def __init__(self, routes: list[_ExtendedURLPattern | URLRouter]) -> None: ... + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + +class ChannelNameRouter: + application_mapping: dict[str, _ChannelApplication] + + def __init__(self, application_mapping: dict[str, _ChannelApplication]) -> None: ... + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... diff --git a/stubs/channels/channels/security/__init__.pyi b/stubs/channels/channels/security/__init__.pyi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/stubs/channels/channels/security/websocket.pyi b/stubs/channels/channels/security/websocket.pyi new file mode 100644 index 000000000000..aa76a1b4012e --- /dev/null +++ b/stubs/channels/channels/security/websocket.pyi @@ -0,0 +1,25 @@ +from collections.abc import Iterable +from re import Pattern +from typing import Any +from urllib.parse import ParseResult + +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable +from channels.consumer import _ChannelScope +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.utils import _ChannelApplication + +class OriginValidator: + application: _ChannelApplication + allowed_origins: Iterable[str | Pattern[str]] + + def __init__(self, application: _ChannelApplication, allowed_origins: Iterable[str | Pattern[str]]) -> None: ... + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Any: ... + def valid_origin(self, parsed_origin: ParseResult | None) -> bool: ... + def validate_origin(self, parsed_origin: ParseResult | None) -> bool: ... + def match_allowed_origin(self, parsed_origin: ParseResult | None, pattern: str | Pattern[str]) -> bool: ... + def get_origin_port(self, origin: ParseResult | None) -> int | None: ... + +def AllowedHostsOriginValidator(application: _ChannelApplication) -> OriginValidator: ... + +class WebsocketDenier(AsyncWebsocketConsumer): + async def connect(self) -> None: ... diff --git a/stubs/channels/channels/sessions.pyi b/stubs/channels/channels/sessions.pyi new file mode 100644 index 000000000000..85b73adadf9f --- /dev/null +++ b/stubs/channels/channels/sessions.pyi @@ -0,0 +1,53 @@ +import datetime +from collections.abc import Awaitable +from typing import Any + +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable +from channels.consumer import _ChannelScope +from channels.utils import _ChannelApplication + +class CookieMiddleware: + inner: _ChannelApplication + + def __init__(self, inner: _ChannelApplication) -> None: ... + async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Any: ... + @classmethod + def set_cookie( + cls, + message: dict[str, Any], + key: str, + value: str = "", + max_age: int | None = ..., + expires: str | datetime.datetime | None = ..., + path: str = ..., + domain: str | None = ..., + secure: bool = ..., + httponly: bool = ..., + samesite: str = ..., + ) -> None: ... + @classmethod + def delete_cookie(cls, message: dict[str, Any], key: str, path: str = ..., domain: str | None = ...) -> None: ... + +class InstanceSessionWrapper: + save_message_types: list[str] + cookie_response_message_types: list[str] + cookie_name: str + session_store: Any + scope: _ChannelScope + activated: bool + real_send: ASGISendCallable + + def __init__(self, scope: _ChannelScope, send: ASGISendCallable) -> None: ... + async def resolve_session(self) -> None: ... + async def send(self, message: dict[str, Any]) -> Awaitable[None]: ... + async def save_session(self) -> None: ... + +class SessionMiddleware: + inner: _ChannelApplication + + def __init__(self, inner: _ChannelApplication) -> None: ... + async def __call__( + self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable + ) -> _ChannelApplication: ... + +def SessionMiddlewareStack(inner: _ChannelApplication) -> _ChannelApplication: ... diff --git a/stubs/channels/channels/testing/__init__.pyi b/stubs/channels/channels/testing/__init__.pyi new file mode 100644 index 000000000000..1cb75a0bf6dd --- /dev/null +++ b/stubs/channels/channels/testing/__init__.pyi @@ -0,0 +1,6 @@ +from .application import ApplicationCommunicator +from .http import HttpCommunicator +from .live import ChannelsLiveServerTestCase +from .websocket import WebsocketCommunicator + +__all__ = ["ApplicationCommunicator", "HttpCommunicator", "ChannelsLiveServerTestCase", "WebsocketCommunicator"] diff --git a/stubs/channels/channels/testing/application.pyi b/stubs/channels/channels/testing/application.pyi new file mode 100644 index 000000000000..313d10cc52d6 --- /dev/null +++ b/stubs/channels/channels/testing/application.pyi @@ -0,0 +1,12 @@ +from typing import Any + +from asgiref.testing import ApplicationCommunicator as BaseApplicationCommunicator + +def no_op() -> None: ... + +class ApplicationCommunicator(BaseApplicationCommunicator): + async def send_input(self, message: dict[str, Any]) -> None: ... + async def receive_output(self, timeout: float = ...) -> dict[str, Any]: ... + async def receive_nothing(self, timeout: float = ..., interval: float = ...) -> bool: ... + async def wait(self, timeout: float = ...) -> None: ... + def stop(self, exceptions: bool = ...) -> None: ... diff --git a/stubs/channels/channels/testing/http.pyi b/stubs/channels/channels/testing/http.pyi new file mode 100644 index 000000000000..3f243e77c47f --- /dev/null +++ b/stubs/channels/channels/testing/http.pyi @@ -0,0 +1,39 @@ +from collections.abc import Iterable +from typing import Literal, TypedDict + +from channels.testing.application import ApplicationCommunicator +from channels.utils import _ChannelApplication + +# HTTP test-specific response type +class _HTTPTestResponse(TypedDict, total=False): + status: int + headers: Iterable[tuple[bytes, bytes]] + body: bytes + +class _HTTPTestScope(TypedDict, total=False): + type: Literal["http"] + http_version: str + method: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] | None + client: tuple[str, int] | None + server: tuple[str, int | None] | None + +class HttpCommunicator(ApplicationCommunicator): + scope: _HTTPTestScope + body: bytes + sent_request: bool + + def __init__( + self, + application: _ChannelApplication, + method: str, + path: str, + body: bytes = ..., + headers: Iterable[tuple[bytes, bytes]] | None = ..., + ) -> None: ... + async def get_response(self, timeout: float = ...) -> _HTTPTestResponse: ... diff --git a/stubs/channels/channels/testing/live.pyi b/stubs/channels/channels/testing/live.pyi new file mode 100644 index 000000000000..c80e5cc3c721 --- /dev/null +++ b/stubs/channels/channels/testing/live.pyi @@ -0,0 +1,34 @@ +from collections.abc import Callable +from typing import Any, ClassVar, TypeAlias + +from channels.routing import ProtocolTypeRouter +from channels.utils import _ChannelApplication +from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.sqlite3.base import DatabaseWrapper +from django.test.testcases import TransactionTestCase +from django.test.utils import modify_settings + +DaphneProcess: TypeAlias = Any + +_StaticWrapper: TypeAlias = Callable[[ProtocolTypeRouter], _ChannelApplication] + +def make_application(*, static_wrapper: _StaticWrapper | None) -> Any: ... + +class ChannelsLiveServerTestCase(TransactionTestCase): + host: ClassVar[str] = ... + ProtocolServerProcess: ClassVar[type[DaphneProcess]] = ... + static_wrapper: ClassVar[type[ASGIStaticFilesHandler]] = ... + serve_static: ClassVar[bool] = ... + + _port: int + _server_process: DaphneProcess + _live_server_modified_settings: modify_settings + + @property + def live_server_url(self) -> str: ... + @property + def live_server_ws_url(self) -> str: ... + def _pre_setup(self) -> None: ... + def _post_teardown(self) -> None: ... + def _is_in_memory_db(self, connection: BaseDatabaseWrapper | DatabaseWrapper) -> bool: ... diff --git a/stubs/channels/channels/testing/websocket.pyi b/stubs/channels/channels/testing/websocket.pyi new file mode 100644 index 000000000000..b9df1aa1b537 --- /dev/null +++ b/stubs/channels/channels/testing/websocket.pyi @@ -0,0 +1,50 @@ +from collections.abc import Iterable +from typing import Any, Literal, TypeAlias, TypedDict, overload +from typing_extensions import NotRequired + +from asgiref.typing import ASGIVersions +from channels.testing.application import ApplicationCommunicator +from channels.utils import _ChannelApplication + +class _WebsocketTestScope(TypedDict, total=False): + spec_version: int + type: Literal["websocket"] + asgi: ASGIVersions + http_version: str + scheme: str + path: str + raw_path: bytes + query_string: bytes + root_path: str + headers: Iterable[tuple[bytes, bytes]] | None + client: tuple[str, int] | None + server: tuple[str, int | None] | None + subprotocols: Iterable[str] | None + state: NotRequired[dict[str, Any]] + extensions: dict[str, dict[object, object]] | None + +_Connected: TypeAlias = bool +_CloseCodeOrAcceptSubProtocol: TypeAlias = int | str | None +_WebsocketConnectResponse: TypeAlias = tuple[_Connected, _CloseCodeOrAcceptSubProtocol] + +class WebsocketCommunicator(ApplicationCommunicator): + scope: _WebsocketTestScope + response_headers: list[tuple[bytes, bytes]] | None + + def __init__( + self, + application: _ChannelApplication, + path: str, + headers: Iterable[tuple[bytes, bytes]] | None = ..., + subprotocols: Iterable[str] | None = ..., + spec_version: int | None = ..., + ) -> None: ... + async def connect(self, timeout: float = ...) -> _WebsocketConnectResponse: ... + async def send_to(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + @overload + async def send_json_to(self, data: dict[str, Any]) -> None: ... + @overload + async def send_json_to(self, data: Any) -> None: ... + async def receive_from(self, timeout: float = ...) -> str | bytes: ... + async def receive_json_from(self, timeout: float = ...) -> dict[str, Any]: ... + async def disconnect(self, code: int = ..., timeout: float = ...) -> None: ... diff --git a/stubs/channels/channels/utils.pyi b/stubs/channels/channels/utils.pyi new file mode 100644 index 000000000000..5567648f1f31 --- /dev/null +++ b/stubs/channels/channels/utils.pyi @@ -0,0 +1,15 @@ +from collections.abc import Awaitable, Callable +from typing import Any, Protocol, TypeAlias + +from asgiref.typing import ASGIApplication, ASGIReceiveCallable + +def name_that_thing(thing: Any) -> str: ... +async def await_many_dispatch( + consumer_callables: list[Callable[[], Awaitable[ASGIReceiveCallable]]], dispatch: Callable[[dict[str, Any]], Awaitable[None]] +) -> None: ... + +class _MiddlewareProtocol(Protocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + async def __call__(self, scope: Any, receive: Any, send: Any) -> Any: ... + +_ChannelApplication: TypeAlias = _MiddlewareProtocol | ASGIApplication # noqa: Y047 diff --git a/stubs/channels/channels/worker.pyi b/stubs/channels/channels/worker.pyi new file mode 100644 index 000000000000..08d979ac76a9 --- /dev/null +++ b/stubs/channels/channels/worker.pyi @@ -0,0 +1,13 @@ +from asgiref.server import StatelessServer +from channels.layers import BaseChannelLayer +from channels.utils import _ChannelApplication + +class Worker(StatelessServer): + channels: list[str] + channel_layer: BaseChannelLayer + + def __init__( + self, application: _ChannelApplication, channels: list[str], channel_layer: BaseChannelLayer, max_applications: int = ... + ) -> None: ... + async def handle(self) -> None: ... + async def listener(self, channel: str) -> None: ... From 7bd1d2f079d023c7fd0d45ef3950159b7f1b6cbd Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 5 May 2025 00:50:25 +0700 Subject: [PATCH 02/14] Fix alias type for python 3.9 --- stubs/channels/METADATA.toml | 1 - stubs/channels/channels/apps.pyi | 3 +-- stubs/channels/channels/layers.pyi | 4 ++-- stubs/channels/channels/testing/live.pyi | 3 ++- stubs/channels/channels/testing/websocket.pyi | 4 ++-- stubs/channels/channels/utils.pyi | 3 ++- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/stubs/channels/METADATA.toml b/stubs/channels/METADATA.toml index 7d980d0a436f..632d96ee0d46 100644 --- a/stubs/channels/METADATA.toml +++ b/stubs/channels/METADATA.toml @@ -1,7 +1,6 @@ version = "4.*" upstream_repository = "https://github.com/django/channels" requires = ["django-stubs", "asgiref"] -requires_python = ">=3.10" [tool.stubtest] skip = true # due to the need of django mypy plugin config, it should be skipped. diff --git a/stubs/channels/channels/apps.pyi b/stubs/channels/channels/apps.pyi index 9d479a2d49e5..72007d91886c 100644 --- a/stubs/channels/channels/apps.pyi +++ b/stubs/channels/channels/apps.pyi @@ -1,6 +1,5 @@ from django.apps import AppConfig -from django.utils.functional import _StrOrPromise class ChannelsConfig(AppConfig): name: str = ... - verbose_name: _StrOrPromise = ... + verbose_name: str = ... diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi index 46c90bbd6821..022e03f38d96 100644 --- a/stubs/channels/channels/layers.pyi +++ b/stubs/channels/channels/layers.pyi @@ -1,7 +1,7 @@ import asyncio from re import Pattern -from typing import Any, TypeAlias, overload -from typing_extensions import deprecated +from typing import Any, overload +from typing_extensions import TypeAlias, deprecated class ChannelLayerManager: backends: dict[str, BaseChannelLayer] diff --git a/stubs/channels/channels/testing/live.pyi b/stubs/channels/channels/testing/live.pyi index c80e5cc3c721..68847948c353 100644 --- a/stubs/channels/channels/testing/live.pyi +++ b/stubs/channels/channels/testing/live.pyi @@ -1,5 +1,6 @@ from collections.abc import Callable -from typing import Any, ClassVar, TypeAlias +from typing import Any, ClassVar +from typing_extensions import TypeAlias from channels.routing import ProtocolTypeRouter from channels.utils import _ChannelApplication diff --git a/stubs/channels/channels/testing/websocket.pyi b/stubs/channels/channels/testing/websocket.pyi index b9df1aa1b537..5aab5799352e 100644 --- a/stubs/channels/channels/testing/websocket.pyi +++ b/stubs/channels/channels/testing/websocket.pyi @@ -1,6 +1,6 @@ from collections.abc import Iterable -from typing import Any, Literal, TypeAlias, TypedDict, overload -from typing_extensions import NotRequired +from typing import Any, Literal, TypedDict, overload +from typing_extensions import NotRequired, TypeAlias from asgiref.typing import ASGIVersions from channels.testing.application import ApplicationCommunicator diff --git a/stubs/channels/channels/utils.pyi b/stubs/channels/channels/utils.pyi index 5567648f1f31..58cbdc9500ea 100644 --- a/stubs/channels/channels/utils.pyi +++ b/stubs/channels/channels/utils.pyi @@ -1,5 +1,6 @@ from collections.abc import Awaitable, Callable -from typing import Any, Protocol, TypeAlias +from typing import Any, Protocol +from typing_extensions import TypeAlias from asgiref.typing import ASGIApplication, ASGIReceiveCallable From dda52a89f9a0f23351d4698fc30f6b79831769d8 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 5 May 2025 01:12:20 +0700 Subject: [PATCH 03/14] Correct some Any type channel layer --- stubs/channels/channels/consumer.pyi | 3 ++- stubs/channels/channels/generic/websocket.pyi | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi index bb7c8e332cee..3a1c57fc7451 100644 --- a/stubs/channels/channels/consumer.pyi +++ b/stubs/channels/channels/consumer.pyi @@ -4,6 +4,7 @@ from typing import Any, ClassVar, Protocol from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WebSocketScope from channels.auth import UserLazyObject from channels.db import database_sync_to_async +from channels.layers import BaseChannelLayer from django.contrib.sessions.backends.base import SessionBase from django.utils.functional import LazyObject @@ -35,7 +36,7 @@ class AsyncConsumer: channel_layer_alias: ClassVar[str] = ... scope: _ChannelScope - channel_layer: Any + channel_layer: BaseChannelLayer channel_name: str channel_receive: ASGIReceiveCallable base_send: ASGISendCallable diff --git a/stubs/channels/channels/generic/websocket.pyi b/stubs/channels/channels/generic/websocket.pyi index aa1a54443560..c380d0c14dc8 100644 --- a/stubs/channels/channels/generic/websocket.pyi +++ b/stubs/channels/channels/generic/websocket.pyi @@ -2,12 +2,13 @@ from typing import Any from asgiref.typing import WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveEvent from channels.consumer import AsyncConsumer, SyncConsumer, _ChannelScope +from channels.layers import BaseChannelLayer class WebsocketConsumer(SyncConsumer): groups: list[str] | None scope: _ChannelScope channel_name: str - channel_layer: Any + channel_layer: BaseChannelLayer channel_receive: Any base_send: Any @@ -37,7 +38,7 @@ class AsyncWebsocketConsumer(AsyncConsumer): groups: list[str] | None scope: _ChannelScope channel_name: str - channel_layer: Any + channel_layer: BaseChannelLayer channel_receive: Any base_send: Any From d30bb2e4736afb3b5db2c6551a61fa5907d64f2c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 5 May 2025 12:29:30 +0700 Subject: [PATCH 04/14] Add type_check_only, TODO comment, and constrain django-stubs version --- stubs/channels/METADATA.toml | 4 ++-- stubs/channels/channels/routing.pyi | 3 ++- stubs/channels/channels/testing/http.pyi | 3 ++- stubs/channels/channels/testing/live.pyi | 2 +- stubs/channels/channels/testing/websocket.pyi | 3 ++- stubs/channels/channels/utils.pyi | 4 ++-- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/stubs/channels/METADATA.toml b/stubs/channels/METADATA.toml index 632d96ee0d46..cf12e31aa2eb 100644 --- a/stubs/channels/METADATA.toml +++ b/stubs/channels/METADATA.toml @@ -1,7 +1,7 @@ version = "4.*" upstream_repository = "https://github.com/django/channels" -requires = ["django-stubs", "asgiref"] +requires = ["django-stubs>=4.2,<5.3", "asgiref"] [tool.stubtest] -skip = true # due to the need of django mypy plugin config, it should be skipped. +skip = true # TODO: enable stubtest once Django mypy plugin config is supported stubtest_requirements = ["daphne"] diff --git a/stubs/channels/channels/routing.pyi b/stubs/channels/channels/routing.pyi index ac98a8e4d7fd..94d025e24985 100644 --- a/stubs/channels/channels/routing.pyi +++ b/stubs/channels/channels/routing.pyi @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, type_check_only from asgiref.typing import ASGIReceiveCallable, ASGISendCallable from django.urls.resolvers import URLPattern @@ -14,6 +14,7 @@ class ProtocolTypeRouter: def __init__(self, application_mapping: dict[str, Any]) -> None: ... async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... +@type_check_only class _ExtendedURLPattern(URLPattern): callback: _ASGIApplicationProtocol | URLRouter diff --git a/stubs/channels/channels/testing/http.pyi b/stubs/channels/channels/testing/http.pyi index 3f243e77c47f..51d52b194fc3 100644 --- a/stubs/channels/channels/testing/http.pyi +++ b/stubs/channels/channels/testing/http.pyi @@ -1,5 +1,5 @@ from collections.abc import Iterable -from typing import Literal, TypedDict +from typing import Literal, TypedDict, type_check_only from channels.testing.application import ApplicationCommunicator from channels.utils import _ChannelApplication @@ -10,6 +10,7 @@ class _HTTPTestResponse(TypedDict, total=False): headers: Iterable[tuple[bytes, bytes]] body: bytes +@type_check_only class _HTTPTestScope(TypedDict, total=False): type: Literal["http"] http_version: str diff --git a/stubs/channels/channels/testing/live.pyi b/stubs/channels/channels/testing/live.pyi index 68847948c353..16322bbe59f4 100644 --- a/stubs/channels/channels/testing/live.pyi +++ b/stubs/channels/channels/testing/live.pyi @@ -10,7 +10,7 @@ from django.db.backends.sqlite3.base import DatabaseWrapper from django.test.testcases import TransactionTestCase from django.test.utils import modify_settings -DaphneProcess: TypeAlias = Any +DaphneProcess: TypeAlias = Any # TODO: temporary hack for daphne.testing.DaphneProcess; remove once daphne provides types _StaticWrapper: TypeAlias = Callable[[ProtocolTypeRouter], _ChannelApplication] diff --git a/stubs/channels/channels/testing/websocket.pyi b/stubs/channels/channels/testing/websocket.pyi index 5aab5799352e..c181d57f8b66 100644 --- a/stubs/channels/channels/testing/websocket.pyi +++ b/stubs/channels/channels/testing/websocket.pyi @@ -1,11 +1,12 @@ from collections.abc import Iterable -from typing import Any, Literal, TypedDict, overload +from typing import Any, Literal, TypedDict, overload, type_check_only from typing_extensions import NotRequired, TypeAlias from asgiref.typing import ASGIVersions from channels.testing.application import ApplicationCommunicator from channels.utils import _ChannelApplication +@type_check_only class _WebsocketTestScope(TypedDict, total=False): spec_version: int type: Literal["websocket"] diff --git a/stubs/channels/channels/utils.pyi b/stubs/channels/channels/utils.pyi index 58cbdc9500ea..b92c892b04e3 100644 --- a/stubs/channels/channels/utils.pyi +++ b/stubs/channels/channels/utils.pyi @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from typing import Any, Protocol +from typing import Any, Protocol, type_check_only from typing_extensions import TypeAlias from asgiref.typing import ASGIApplication, ASGIReceiveCallable @@ -8,7 +8,7 @@ def name_that_thing(thing: Any) -> str: ... async def await_many_dispatch( consumer_callables: list[Callable[[], Awaitable[ASGIReceiveCallable]]], dispatch: Callable[[dict[str, Any]], Awaitable[None]] ) -> None: ... - +@type_check_only class _MiddlewareProtocol(Protocol): def __init__(self, *args: Any, **kwargs: Any) -> None: ... async def __call__(self, scope: Any, receive: Any, send: Any) -> Any: ... From 6d15454c6f7766e47485fc71675a48d69084199a Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 5 May 2025 12:43:50 +0700 Subject: [PATCH 05/14] Add type_check_only for channels consumer and testing http --- stubs/channels/channels/consumer.pyi | 6 ++++-- stubs/channels/channels/testing/http.pyi | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi index 3a1c57fc7451..d975b70735e1 100644 --- a/stubs/channels/channels/consumer.pyi +++ b/stubs/channels/channels/consumer.pyi @@ -1,5 +1,5 @@ from collections.abc import Awaitable -from typing import Any, ClassVar, Protocol +from typing import Any, ClassVar, Protocol, type_check_only from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WebSocketScope from channels.auth import UserLazyObject @@ -8,10 +8,12 @@ from channels.layers import BaseChannelLayer from django.contrib.sessions.backends.base import SessionBase from django.utils.functional import LazyObject +@type_check_only class _LazySession(SessionBase, LazyObject): # type: ignore[misc] _wrapped: SessionBase # Base ASGI Scope definition +@type_check_only class _ChannelScope(WebSocketScope, total=False): # Channel specific channel: str @@ -24,7 +26,7 @@ class _ChannelScope(WebSocketScope, total=False): user: UserLazyObject | None def get_handler_name(message: dict[str, Any]) -> str: ... - +@type_check_only class _ASGIApplicationProtocol(Protocol): consumer_class: Any consumer_initkwargs: dict[str, Any] diff --git a/stubs/channels/channels/testing/http.pyi b/stubs/channels/channels/testing/http.pyi index 51d52b194fc3..0e72190f3198 100644 --- a/stubs/channels/channels/testing/http.pyi +++ b/stubs/channels/channels/testing/http.pyi @@ -5,6 +5,7 @@ from channels.testing.application import ApplicationCommunicator from channels.utils import _ChannelApplication # HTTP test-specific response type +@type_check_only class _HTTPTestResponse(TypedDict, total=False): status: int headers: Iterable[tuple[bytes, bytes]] From 916f020108fe4d8884acff95b0e8d27e40956b00 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 6 May 2025 11:44:44 +0700 Subject: [PATCH 06/14] Add stubtest --- stubs/channels/@tests/django_settings.py | 12 ++++++++++++ stubs/channels/@tests/stubtest_allowlist.txt | 3 +++ stubs/channels/METADATA.toml | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 stubs/channels/@tests/django_settings.py create mode 100644 stubs/channels/@tests/stubtest_allowlist.txt diff --git a/stubs/channels/@tests/django_settings.py b/stubs/channels/@tests/django_settings.py new file mode 100644 index 000000000000..2be16834be19 --- /dev/null +++ b/stubs/channels/@tests/django_settings.py @@ -0,0 +1,12 @@ +SECRET_KEY = "1" + +INSTALLED_APPS = ( + "django.contrib.contenttypes", + "django.contrib.sites", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.admin.apps.SimpleAdminConfig", + "django.contrib.staticfiles", + "django.contrib.auth", + "channels", +) diff --git a/stubs/channels/@tests/stubtest_allowlist.txt b/stubs/channels/@tests/stubtest_allowlist.txt new file mode 100644 index 000000000000..0891cbe07efa --- /dev/null +++ b/stubs/channels/@tests/stubtest_allowlist.txt @@ -0,0 +1,3 @@ +channels.auth.UserLazyObject +channels.auth.UserLazyObject.* +channels.db.database_sync_to_async diff --git a/stubs/channels/METADATA.toml b/stubs/channels/METADATA.toml index cf12e31aa2eb..9ed6485c6a8b 100644 --- a/stubs/channels/METADATA.toml +++ b/stubs/channels/METADATA.toml @@ -3,5 +3,6 @@ upstream_repository = "https://github.com/django/channels" requires = ["django-stubs>=4.2,<5.3", "asgiref"] [tool.stubtest] -skip = true # TODO: enable stubtest once Django mypy plugin config is supported +mypy_plugins = ['mypy_django_plugin.main'] +mypy_plugins_config = {"django-stubs" = {"django_settings_module" = "@tests.django_settings"}} stubtest_requirements = ["daphne"] From 1abe09c95658e5ff6b66544bf49edf5fb4c5a381 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 12 May 2025 22:17:05 +0700 Subject: [PATCH 07/14] Migrate database_sync_to_async to async def for auth.pyi functions --- stubs/channels/channels/auth.pyi | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/stubs/channels/channels/auth.pyi b/stubs/channels/channels/auth.pyi index 5f3b7b5e20fe..44b657e79112 100644 --- a/stubs/channels/channels/auth.pyi +++ b/stubs/channels/channels/auth.pyi @@ -8,15 +8,11 @@ from django.contrib.auth.models import AnonymousUser from django.utils.functional import LazyObject from .consumer import _ChannelScope, _LazySession -from .db import database_sync_to_async from .utils import _ChannelApplication -@database_sync_to_async -def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ... -@database_sync_to_async -def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = ...) -> None: ... -@database_sync_to_async -def logout(scope: _ChannelScope) -> None: ... +async def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ... +async def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = ...) -> None: ... +async def logout(scope: _ChannelScope) -> None: ... def _get_user_session_key(session: _LazySession) -> Any: ... class UserLazyObject(AbstractBaseUser, LazyObject): From 6b01c8ac384a16a98934b6556e01d630f78b36e3 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 12 May 2025 22:18:07 +0700 Subject: [PATCH 08/14] Remove internal function type definitions --- stubs/channels/channels/auth.pyi | 8 ++------ stubs/channels/channels/layers.pyi | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/stubs/channels/channels/auth.pyi b/stubs/channels/channels/auth.pyi index 44b657e79112..2187261b096a 100644 --- a/stubs/channels/channels/auth.pyi +++ b/stubs/channels/channels/auth.pyi @@ -1,5 +1,3 @@ -from typing import Any - from asgiref.typing import ASGIReceiveCallable, ASGISendCallable from channels.middleware import BaseMiddleware from django.contrib.auth.backends import BaseBackend @@ -7,16 +5,14 @@ from django.contrib.auth.base_user import AbstractBaseUser from django.contrib.auth.models import AnonymousUser from django.utils.functional import LazyObject -from .consumer import _ChannelScope, _LazySession +from .consumer import _ChannelScope from .utils import _ChannelApplication async def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ... async def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = ...) -> None: ... async def logout(scope: _ChannelScope) -> None: ... -def _get_user_session_key(session: _LazySession) -> Any: ... -class UserLazyObject(AbstractBaseUser, LazyObject): - def _setup(self) -> None: ... +class UserLazyObject(AbstractBaseUser, LazyObject): ... class AuthMiddleware(BaseMiddleware): def populate_scope(self, scope: _ChannelScope) -> None: ... diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi index 022e03f38d96..ef4346b6af09 100644 --- a/stubs/channels/channels/layers.pyi +++ b/stubs/channels/channels/layers.pyi @@ -7,12 +7,10 @@ class ChannelLayerManager: backends: dict[str, BaseChannelLayer] def __init__(self) -> None: ... - def _reset_backends(self, setting: str, **kwargs: Any) -> None: ... @property def configs(self) -> dict[str, Any]: ... def make_backend(self, name: str) -> BaseChannelLayer: ... def make_test_backend(self, name: str) -> Any: ... - def _make_backend(self, name: str, config: dict[str, Any]) -> BaseChannelLayer: ... def __getitem__(self, key: str) -> BaseChannelLayer: ... def __contains__(self, key: str) -> bool: ... def set(self, key: str, layer: BaseChannelLayer) -> BaseChannelLayer | None: ... From bd570f5d1ca179549593bd5d64528ebcd9d86105 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 13 May 2025 10:37:03 +0700 Subject: [PATCH 09/14] Remove internal function type definitions (more) --- stubs/channels/channels/layers.pyi | 2 -- stubs/channels/channels/testing/live.pyi | 5 ----- 2 files changed, 7 deletions(-) diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi index ef4346b6af09..b803b9367640 100644 --- a/stubs/channels/channels/layers.pyi +++ b/stubs/channels/channels/layers.pyi @@ -81,10 +81,8 @@ class InMemoryChannelLayer(BaseChannelLayer): async def send(self, channel: str, message: dict[str, Any]) -> None: ... async def receive(self, channel: str) -> dict[str, Any]: ... async def new_channel(self, prefix: str = ...) -> str: ... - def _clean_expired(self) -> None: ... async def flush(self) -> None: ... async def close(self) -> None: ... - def _remove_from_groups(self, channel: str) -> None: ... async def group_add(self, group: str, channel: str) -> None: ... async def group_discard(self, group: str, channel: str) -> None: ... async def group_send(self, group: str, message: dict[str, Any]) -> None: ... diff --git a/stubs/channels/channels/testing/live.pyi b/stubs/channels/channels/testing/live.pyi index 16322bbe59f4..f3c9571460c7 100644 --- a/stubs/channels/channels/testing/live.pyi +++ b/stubs/channels/channels/testing/live.pyi @@ -5,8 +5,6 @@ from typing_extensions import TypeAlias from channels.routing import ProtocolTypeRouter from channels.utils import _ChannelApplication from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler -from django.db.backends.base.base import BaseDatabaseWrapper -from django.db.backends.sqlite3.base import DatabaseWrapper from django.test.testcases import TransactionTestCase from django.test.utils import modify_settings @@ -30,6 +28,3 @@ class ChannelsLiveServerTestCase(TransactionTestCase): def live_server_url(self) -> str: ... @property def live_server_ws_url(self) -> str: ... - def _pre_setup(self) -> None: ... - def _post_teardown(self) -> None: ... - def _is_in_memory_db(self, connection: BaseDatabaseWrapper | DatabaseWrapper) -> bool: ... From 050fd7992ac54a987f797c7d125e95d6ea6c6a37 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 13 May 2025 22:56:15 +0700 Subject: [PATCH 10/14] Correct Channels typo (plural) --- stubs/channels/channels/consumer.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi index d975b70735e1..a41249da4b90 100644 --- a/stubs/channels/channels/consumer.pyi +++ b/stubs/channels/channels/consumer.pyi @@ -15,7 +15,7 @@ class _LazySession(SessionBase, LazyObject): # type: ignore[misc] # Base ASGI Scope definition @type_check_only class _ChannelScope(WebSocketScope, total=False): - # Channel specific + # Channels specific channel: str url_route: dict[str, Any] path_remaining: str From 4f80ba13b116787ae493c8834d1276f902aff0e2 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 9 Jul 2025 23:51:23 +0700 Subject: [PATCH 11/14] Improve Django Channels type stubs with better annotations and parameter defaults --- stubs/channels/@tests/stubtest_allowlist.txt | 7 +++- stubs/channels/METADATA.toml | 2 +- stubs/channels/channels/__init__.pyi | 6 ++-- stubs/channels/channels/apps.pyi | 4 +-- stubs/channels/channels/auth.pyi | 4 ++- stubs/channels/channels/consumer.pyi | 32 ++++++++++++++----- stubs/channels/channels/db.pyi | 9 +++++- stubs/channels/channels/generic/http.pyi | 8 ++--- stubs/channels/channels/generic/websocket.pyi | 24 +++++++------- stubs/channels/channels/layers.pyi | 24 +++++++------- .../management/commands/runworker.pyi | 14 +++++--- stubs/channels/channels/routing.pyi | 7 ++-- stubs/channels/channels/sessions.pyi | 19 +++++------ .../channels/channels/testing/application.pyi | 14 +++++--- stubs/channels/channels/testing/http.pyi | 6 ++-- stubs/channels/channels/testing/live.pyi | 9 ++---- stubs/channels/channels/testing/websocket.pyi | 19 ++++++----- stubs/channels/channels/utils.pyi | 4 +++ stubs/channels/channels/worker.pyi | 2 +- 19 files changed, 128 insertions(+), 86 deletions(-) diff --git a/stubs/channels/@tests/stubtest_allowlist.txt b/stubs/channels/@tests/stubtest_allowlist.txt index 0891cbe07efa..6e1d93f95ec5 100644 --- a/stubs/channels/@tests/stubtest_allowlist.txt +++ b/stubs/channels/@tests/stubtest_allowlist.txt @@ -1,3 +1,8 @@ -channels.auth.UserLazyObject +# channels.auth.UserLazyObject.DoesNotExist is not present at runtime +# channels.auth.UserLazyObject.MultipleObjectsReturned is not present at runtime +# channels.auth.UserLazyObject@AnnotatedWith is not present at runtime channels.auth.UserLazyObject.* + +# database_sync_to_async is implemented as a class instance but stubbed as a function +# for better type inference when used as decorator/function channels.db.database_sync_to_async diff --git a/stubs/channels/METADATA.toml b/stubs/channels/METADATA.toml index 9ed6485c6a8b..a5df088dbbeb 100644 --- a/stubs/channels/METADATA.toml +++ b/stubs/channels/METADATA.toml @@ -1,4 +1,4 @@ -version = "4.*" +version = "4.2.*" upstream_repository = "https://github.com/django/channels" requires = ["django-stubs>=4.2,<5.3", "asgiref"] diff --git a/stubs/channels/channels/__init__.pyi b/stubs/channels/channels/__init__.pyi index ae22f453163e..561199954632 100644 --- a/stubs/channels/channels/__init__.pyi +++ b/stubs/channels/channels/__init__.pyi @@ -1,2 +1,4 @@ -__version__: str -DEFAULT_CHANNEL_LAYER: str +from typing import Final + +__version__: Final[str] +DEFAULT_CHANNEL_LAYER: Final[str] diff --git a/stubs/channels/channels/apps.pyi b/stubs/channels/channels/apps.pyi index 72007d91886c..33de39dc4c89 100644 --- a/stubs/channels/channels/apps.pyi +++ b/stubs/channels/channels/apps.pyi @@ -1,5 +1,5 @@ from django.apps import AppConfig class ChannelsConfig(AppConfig): - name: str = ... - verbose_name: str = ... + name: str = "channels" + verbose_name: str = "Channels" diff --git a/stubs/channels/channels/auth.pyi b/stubs/channels/channels/auth.pyi index 2187261b096a..b8594667e059 100644 --- a/stubs/channels/channels/auth.pyi +++ b/stubs/channels/channels/auth.pyi @@ -9,9 +9,11 @@ from .consumer import _ChannelScope from .utils import _ChannelApplication async def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ... -async def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = ...) -> None: ... +async def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = None) -> None: ... async def logout(scope: _ChannelScope) -> None: ... +# Inherits AbstractBaseUser to improve autocomplete and show this is a lazy proxy for a user. +# At runtime, it's just a LazyObject that wraps the actual user instance. class UserLazyObject(AbstractBaseUser, LazyObject): ... class AuthMiddleware(BaseMiddleware): diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi index a41249da4b90..27a257921909 100644 --- a/stubs/channels/channels/consumer.pyi +++ b/stubs/channels/channels/consumer.pyi @@ -1,5 +1,5 @@ from collections.abc import Awaitable -from typing import Any, ClassVar, Protocol, type_check_only +from typing import Any, ClassVar, Protocol, TypedDict, type_check_only from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WebSocketScope from channels.auth import UserLazyObject @@ -8,16 +8,27 @@ from channels.layers import BaseChannelLayer from django.contrib.sessions.backends.base import SessionBase from django.utils.functional import LazyObject +# _LazySession is a LazyObject that wraps a SessionBase instance. +# We subclass both for type checking purposes to expose SessionBase attributes, +# and suppress mypy's "misc" error with `# type: ignore[misc]`. @type_check_only class _LazySession(SessionBase, LazyObject): # type: ignore[misc] _wrapped: SessionBase -# Base ASGI Scope definition +@type_check_only +class _URLRoute(TypedDict): + # Values extracted from Django's URLPattern matching, + # passed through ASGI scope routing. + # `args` and `kwargs` are the result of pattern matching against the URL path. + args: tuple[Any, ...] + kwargs: dict[str, Any] + +# Channel Scope definition @type_check_only class _ChannelScope(WebSocketScope, total=False): # Channels specific channel: str - url_route: dict[str, Any] + url_route: _URLRoute path_remaining: str # Auth specific @@ -25,17 +36,21 @@ class _ChannelScope(WebSocketScope, total=False): session: _LazySession user: UserLazyObject | None +# Accepts any ASGI message dict with a required "type" key (str), +# but allows additional arbitrary keys for flexibility. def get_handler_name(message: dict[str, Any]) -> str: ... @type_check_only class _ASGIApplicationProtocol(Protocol): - consumer_class: Any - consumer_initkwargs: dict[str, Any] + consumer_class: AsyncConsumer + + # Accepts any initialization kwargs passed to the consumer class. + # Typed as `Any` to allow flexibility in subclass-specific arguments. + consumer_initkwargs: Any def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Awaitable[None]: ... class AsyncConsumer: - _sync: ClassVar[bool] = ... - channel_layer_alias: ClassVar[str] = ... + channel_layer_alias: ClassVar[str] scope: _ChannelScope channel_layer: BaseChannelLayer @@ -50,8 +65,9 @@ class AsyncConsumer: def as_asgi(cls, **initkwargs: Any) -> _ASGIApplicationProtocol: ... class SyncConsumer(AsyncConsumer): - _sync: ClassVar[bool] = ... + # Since we're overriding asynchronous methods with synchronous ones, + # we need to use `# type: ignore[override]` to suppress mypy errors. @database_sync_to_async def dispatch(self, message: dict[str, Any]) -> None: ... # type: ignore[override] def send(self, message: dict[str, Any]) -> None: ... # type: ignore[override] diff --git a/stubs/channels/channels/db.pyi b/stubs/channels/channels/db.pyi index 98c68d1e21db..7e82ea89b5be 100644 --- a/stubs/channels/channels/db.pyi +++ b/stubs/channels/channels/db.pyi @@ -1,5 +1,6 @@ from asyncio import BaseEventLoop from collections.abc import Callable, Coroutine +from concurrent.futures import ThreadPoolExecutor from typing import Any, TypeVar from typing_extensions import ParamSpec @@ -11,5 +12,11 @@ _R = TypeVar("_R") class DatabaseSyncToAsync(SyncToAsync[_P, _R]): def thread_handler(self, loop: BaseEventLoop, *args: Any, **kwargs: Any) -> Any: ... -def database_sync_to_async(func: Callable[_P, _R]) -> Callable[_P, Coroutine[Any, Any, _R]]: ... +# We define `database_sync_to_async` as a function instead of assigning +# `DatabaseSyncToAsync(...)` directly, to preserve both decorator and +# higher-order function behavior with correct type hints. +# A direct assignment would result in incorrect type inference for the wrapped function. +def database_sync_to_async( + func: Callable[_P, _R], thread_sensitive: bool = True, executor: ThreadPoolExecutor | None = None +) -> Callable[_P, Coroutine[Any, Any, _R]]: ... async def aclose_old_connections() -> None: ... diff --git a/stubs/channels/channels/generic/http.pyi b/stubs/channels/channels/generic/http.pyi index 85d83a7fd208..84e3b1dbff84 100644 --- a/stubs/channels/channels/generic/http.pyi +++ b/stubs/channels/channels/generic/http.pyi @@ -1,3 +1,4 @@ +from _typeshed import Unused from collections.abc import Iterable from typing import Any @@ -8,12 +9,11 @@ class AsyncHttpConsumer(AsyncConsumer): body: list[bytes] scope: HTTPScope # type: ignore[assignment] - def __init__(self, *args: Any, **kwargs: Any) -> None: ... - async def send_headers(self, *, status: int = ..., headers: Iterable[tuple[bytes, bytes]] | None = ...) -> None: ... - async def send_body(self, body: bytes, *, more_body: bool = ...) -> None: ... + def __init__(self, *args: Unused, **kwargs: Unused) -> None: ... + async def send_headers(self, *, status: int = 200, headers: Iterable[tuple[bytes, bytes]] | None = None) -> None: ... + async def send_body(self, body: bytes, *, more_body: bool = False) -> None: ... async def send_response(self, status: int, body: bytes, **kwargs: Any) -> None: ... async def handle(self, body: bytes) -> None: ... async def disconnect(self) -> None: ... async def http_request(self, message: HTTPRequestEvent) -> None: ... async def http_disconnect(self, message: HTTPDisconnectEvent) -> None: ... - async def send(self, message: dict[str, Any]) -> None: ... # type: ignore[override] diff --git a/stubs/channels/channels/generic/websocket.pyi b/stubs/channels/channels/generic/websocket.pyi index c380d0c14dc8..8dc44744fc80 100644 --- a/stubs/channels/channels/generic/websocket.pyi +++ b/stubs/channels/channels/generic/websocket.pyi @@ -15,20 +15,20 @@ class WebsocketConsumer(SyncConsumer): def __init__(self, *args: Any, **kwargs: Any) -> None: ... def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... def connect(self) -> None: ... - def accept(self, subprotocol: str | None = ..., headers: list[tuple[str, str]] | None = ...) -> None: ... + def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ... def websocket_receive(self, message: WebSocketReceiveEvent) -> None: ... - def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + def receive(self, text_data: str | None = None, bytes_data: bytes | None = None) -> None: ... def send( # type: ignore[override] - self, text_data: str | None = ..., bytes_data: bytes | None = ..., close: bool = ... + self, text_data: str | None = None, bytes_data: bytes | None = None, close: bool = False ) -> None: ... - def close(self, code: int | bool | None = ..., reason: str | None = ...) -> None: ... + def close(self, code: int | bool | None = None, reason: str | None = None) -> None: ... def websocket_disconnect(self, message: WebSocketDisconnectEvent) -> None: ... def disconnect(self, code: int) -> None: ... class JsonWebsocketConsumer(WebsocketConsumer): - def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ..., **kwargs: Any) -> None: ... + def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ... def receive_json(self, content: Any, **kwargs: Any) -> None: ... - def send_json(self, content: Any, close: bool = ...) -> None: ... + def send_json(self, content: Any, close: bool = False) -> None: ... @classmethod def decode_json(cls, text_data: str) -> Any: ... @classmethod @@ -45,20 +45,20 @@ class AsyncWebsocketConsumer(AsyncConsumer): def __init__(self, *args: Any, **kwargs: Any) -> None: ... async def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... async def connect(self) -> None: ... - async def accept(self, subprotocol: str | None = ..., headers: list[tuple[str, str]] | None = ...) -> None: ... + async def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ... async def websocket_receive(self, message: WebSocketReceiveEvent) -> None: ... - async def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + async def receive(self, text_data: str | None = None, bytes_data: bytes | None = None) -> None: ... async def send( # type: ignore[override] - self, text_data: str | None = ..., bytes_data: bytes | None = ..., close: bool = ... + self, text_data: str | None = None, bytes_data: bytes | None = None, close: bool = False ) -> None: ... - async def close(self, code: int | bool | None = ..., reason: str | None = ...) -> None: ... + async def close(self, code: int | bool | None = None, reason: str | None = None) -> None: ... async def websocket_disconnect(self, message: WebSocketDisconnectEvent) -> None: ... async def disconnect(self, code: int) -> None: ... class AsyncJsonWebsocketConsumer(AsyncWebsocketConsumer): - async def receive(self, text_data: str | None = ..., bytes_data: bytes | None = ..., **kwargs: Any) -> None: ... + async def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ... async def receive_json(self, content: Any, **kwargs: Any) -> None: ... - async def send_json(self, content: Any, close: bool = ...) -> None: ... + async def send_json(self, content: Any, close: bool = False) -> None: ... @classmethod async def decode_json(cls, text_data: str) -> Any: ... @classmethod diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi index b803b9367640..f5ab05959be0 100644 --- a/stubs/channels/channels/layers.pyi +++ b/stubs/channels/channels/layers.pyi @@ -1,6 +1,6 @@ import asyncio from re import Pattern -from typing import Any, overload +from typing import Any, ClassVar, overload from typing_extensions import TypeAlias, deprecated class ChannelLayerManager: @@ -20,7 +20,7 @@ _ChannelCapacityDict: TypeAlias = dict[_ChannelCapacityPattern, int] _CompiledChannelCapacity: TypeAlias = list[tuple[Pattern[str], int]] class BaseChannelLayer: - MAX_NAME_LENGTH: int = ... + MAX_NAME_LENGTH: ClassVar[int] = 100 expiry: int capacity: int channel_capacity: _ChannelCapacityDict @@ -28,7 +28,7 @@ class BaseChannelLayer: group_name_regex: Pattern[str] invalid_name_error: str - def __init__(self, expiry: int = ..., capacity: int = ..., channel_capacity: _ChannelCapacityDict | None = ...) -> None: ... + def __init__(self, expiry: int = 60, capacity: int = 100, channel_capacity: _ChannelCapacityDict | None = None) -> None: ... def compile_capacities(self, channel_capacity: _ChannelCapacityDict) -> _CompiledChannelCapacity: ... def get_capacity(self, channel: str) -> int: ... @overload @@ -36,17 +36,17 @@ class BaseChannelLayer: @overload def match_type_and_length(self, name: Any) -> bool: ... @overload - def require_valid_channel_name(self, name: str, receive: bool = ...) -> bool: ... + def require_valid_channel_name(self, name: str, receive: bool = False) -> bool: ... @overload - def require_valid_channel_name(self, name: Any, receive: bool = ...) -> bool: ... + def require_valid_channel_name(self, name: Any, receive: bool = False) -> bool: ... @overload def require_valid_group_name(self, name: str) -> bool: ... @overload def require_valid_group_name(self, name: Any) -> bool: ... @overload - def valid_channel_names(self, names: list[str], receive: bool = ...) -> bool: ... + def valid_channel_names(self, names: list[str], receive: bool = False) -> bool: ... @overload - def valid_channel_names(self, names: list[Any], receive: bool = ...) -> bool: ... + def valid_channel_names(self, names: list[Any], receive: bool = False) -> bool: ... def non_local_name(self, name: str) -> str: ... async def send(self, channel: str, message: dict[str, Any]) -> None: ... async def receive(self, channel: str) -> dict[str, Any]: ... @@ -56,7 +56,7 @@ class BaseChannelLayer: async def group_discard(self, group: str, channel: str) -> None: ... async def group_send(self, group: str, message: dict[str, Any]) -> None: ... @deprecated("Use require_valid_channel_name instead.") - def valid_channel_name(self, channel_name: str, receive: bool = ...) -> bool: ... + def valid_channel_name(self, channel_name: str, receive: bool = False) -> bool: ... @deprecated("Use require_valid_group_name instead.") def valid_group_name(self, group_name: str) -> bool: ... @@ -69,9 +69,9 @@ class InMemoryChannelLayer(BaseChannelLayer): def __init__( self, - expiry: int = ..., - group_expiry: int = ..., - capacity: int = ..., + expiry: int = 60, + group_expiry: int = 86400, + capacity: int = 100, channel_capacity: _ChannelCapacityDict | None = ..., **kwargs: Any, ) -> None: ... @@ -80,7 +80,7 @@ class InMemoryChannelLayer(BaseChannelLayer): async def send(self, channel: str, message: dict[str, Any]) -> None: ... async def receive(self, channel: str) -> dict[str, Any]: ... - async def new_channel(self, prefix: str = ...) -> str: ... + async def new_channel(self, prefix: str = "specific.") -> str: ... async def flush(self) -> None: ... async def close(self) -> None: ... async def group_add(self, group: str, channel: str) -> None: ... diff --git a/stubs/channels/channels/management/commands/runworker.pyi b/stubs/channels/channels/management/commands/runworker.pyi index 1ea4a1e6d4f8..b202effec2dd 100644 --- a/stubs/channels/channels/management/commands/runworker.pyi +++ b/stubs/channels/channels/management/commands/runworker.pyi @@ -1,6 +1,6 @@ import logging from argparse import ArgumentParser -from typing import Any +from typing import Any, TypedDict, type_check_only from channels.layers import BaseChannelLayer from channels.worker import Worker @@ -8,13 +8,17 @@ from django.core.management.base import BaseCommand logger: logging.Logger +@type_check_only +class _RunWorkerCommandOption(TypedDict): + verbosity: int | None + layer: str + channels: list[str] + class Command(BaseCommand): - leave_locale_alone: bool = ... + leave_locale_alone: bool = True worker_class: type[Worker] = ... verbosity: int channel_layer: BaseChannelLayer def add_arguments(self, parser: ArgumentParser) -> None: ... - def handle( - self, *args: Any, application_path: str | None = ..., channels: list[str] | None = ..., layer: str = ..., **options: Any - ) -> None: ... + def handle(self, *args: Any, **options: _RunWorkerCommandOption) -> None: ... diff --git a/stubs/channels/channels/routing.pyi b/stubs/channels/channels/routing.pyi index 94d025e24985..d2a0655d43ce 100644 --- a/stubs/channels/channels/routing.pyi +++ b/stubs/channels/channels/routing.pyi @@ -1,6 +1,6 @@ from typing import Any, type_check_only -from asgiref.typing import ASGIReceiveCallable, ASGISendCallable +from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope from django.urls.resolvers import URLPattern from .consumer import _ASGIApplicationProtocol, _ChannelScope @@ -12,18 +12,17 @@ class ProtocolTypeRouter: application_mapping: dict[str, _ChannelApplication] def __init__(self, application_mapping: dict[str, Any]) -> None: ... - async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... @type_check_only class _ExtendedURLPattern(URLPattern): callback: _ASGIApplicationProtocol | URLRouter class URLRouter: - _path_routing: bool = ... routes: list[_ExtendedURLPattern | URLRouter] def __init__(self, routes: list[_ExtendedURLPattern | URLRouter]) -> None: ... - async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... + async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... class ChannelNameRouter: application_mapping: dict[str, _ChannelApplication] diff --git a/stubs/channels/channels/sessions.pyi b/stubs/channels/channels/sessions.pyi index 85b73adadf9f..fcba517df584 100644 --- a/stubs/channels/channels/sessions.pyi +++ b/stubs/channels/channels/sessions.pyi @@ -5,6 +5,7 @@ from typing import Any from asgiref.typing import ASGIReceiveCallable, ASGISendCallable from channels.consumer import _ChannelScope from channels.utils import _ChannelApplication +from django.contrib.sessions.backends.base import SessionBase class CookieMiddleware: inner: _ChannelApplication @@ -17,22 +18,22 @@ class CookieMiddleware: message: dict[str, Any], key: str, value: str = "", - max_age: int | None = ..., - expires: str | datetime.datetime | None = ..., - path: str = ..., - domain: str | None = ..., - secure: bool = ..., - httponly: bool = ..., - samesite: str = ..., + max_age: int | None = None, + expires: str | datetime.datetime | None = None, + path: str = "/", + domain: str | None = None, + secure: bool = False, + httponly: bool = False, + samesite: str = "lax", ) -> None: ... @classmethod - def delete_cookie(cls, message: dict[str, Any], key: str, path: str = ..., domain: str | None = ...) -> None: ... + def delete_cookie(cls, message: dict[str, Any], key: str, path: str = "/", domain: str | None = None) -> None: ... class InstanceSessionWrapper: save_message_types: list[str] cookie_response_message_types: list[str] cookie_name: str - session_store: Any + session_store: SessionBase scope: _ChannelScope activated: bool real_send: ASGISendCallable diff --git a/stubs/channels/channels/testing/application.pyi b/stubs/channels/channels/testing/application.pyi index 313d10cc52d6..1984a28e32ff 100644 --- a/stubs/channels/channels/testing/application.pyi +++ b/stubs/channels/channels/testing/application.pyi @@ -5,8 +5,12 @@ from asgiref.testing import ApplicationCommunicator as BaseApplicationCommunicat def no_op() -> None: ... class ApplicationCommunicator(BaseApplicationCommunicator): - async def send_input(self, message: dict[str, Any]) -> None: ... - async def receive_output(self, timeout: float = ...) -> dict[str, Any]: ... - async def receive_nothing(self, timeout: float = ..., interval: float = ...) -> bool: ... - async def wait(self, timeout: float = ...) -> None: ... - def stop(self, exceptions: bool = ...) -> None: ... + async def send_input(self, message: Any) -> None: ... + async def receive_output(self, timeout: float = 1) -> Any: ... + + # The following methods are not present in the original source code, + # but are commonly used in practice. Since the base package doesn't + # provide type hints for them, they are added here to improve type correctness. + async def receive_nothing(self, timeout: float = 0.1, interval: float = 0.01) -> bool: ... + async def wait(self, timeout: float = 1) -> None: ... + def stop(self, exceptions: bool = True) -> None: ... diff --git a/stubs/channels/channels/testing/http.pyi b/stubs/channels/channels/testing/http.pyi index 0e72190f3198..6eb6650036c8 100644 --- a/stubs/channels/channels/testing/http.pyi +++ b/stubs/channels/channels/testing/http.pyi @@ -35,7 +35,7 @@ class HttpCommunicator(ApplicationCommunicator): application: _ChannelApplication, method: str, path: str, - body: bytes = ..., - headers: Iterable[tuple[bytes, bytes]] | None = ..., + body: bytes = b"", + headers: Iterable[tuple[bytes, bytes]] | None = None, ) -> None: ... - async def get_response(self, timeout: float = ...) -> _HTTPTestResponse: ... + async def get_response(self, timeout: float = 1) -> _HTTPTestResponse: ... diff --git a/stubs/channels/channels/testing/live.pyi b/stubs/channels/channels/testing/live.pyi index f3c9571460c7..769604458d54 100644 --- a/stubs/channels/channels/testing/live.pyi +++ b/stubs/channels/channels/testing/live.pyi @@ -6,7 +6,6 @@ from channels.routing import ProtocolTypeRouter from channels.utils import _ChannelApplication from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler from django.test.testcases import TransactionTestCase -from django.test.utils import modify_settings DaphneProcess: TypeAlias = Any # TODO: temporary hack for daphne.testing.DaphneProcess; remove once daphne provides types @@ -15,14 +14,10 @@ _StaticWrapper: TypeAlias = Callable[[ProtocolTypeRouter], _ChannelApplication] def make_application(*, static_wrapper: _StaticWrapper | None) -> Any: ... class ChannelsLiveServerTestCase(TransactionTestCase): - host: ClassVar[str] = ... + host: ClassVar[str] = "localhost" ProtocolServerProcess: ClassVar[type[DaphneProcess]] = ... static_wrapper: ClassVar[type[ASGIStaticFilesHandler]] = ... - serve_static: ClassVar[bool] = ... - - _port: int - _server_process: DaphneProcess - _live_server_modified_settings: modify_settings + serve_static: ClassVar[bool] = True @property def live_server_url(self) -> str: ... diff --git a/stubs/channels/channels/testing/websocket.pyi b/stubs/channels/channels/testing/websocket.pyi index c181d57f8b66..f303e90e5c39 100644 --- a/stubs/channels/channels/testing/websocket.pyi +++ b/stubs/channels/channels/testing/websocket.pyi @@ -36,16 +36,19 @@ class WebsocketCommunicator(ApplicationCommunicator): self, application: _ChannelApplication, path: str, - headers: Iterable[tuple[bytes, bytes]] | None = ..., - subprotocols: Iterable[str] | None = ..., - spec_version: int | None = ..., + headers: Iterable[tuple[bytes, bytes]] | None = None, + subprotocols: Iterable[str] | None = None, + spec_version: int | None = None, ) -> None: ... - async def connect(self, timeout: float = ...) -> _WebsocketConnectResponse: ... - async def send_to(self, text_data: str | None = ..., bytes_data: bytes | None = ...) -> None: ... + async def connect(self, timeout: float = 1) -> _WebsocketConnectResponse: ... + async def send_to(self, text_data: str | None = None, bytes_data: bytes | None = None) -> None: ... + async def receive_from(self, timeout: float = 1) -> str | bytes: ... + + # These overloads reflect common usage, where users typically send and receive `dict[str, Any]`. + # The base case allows `Any` to support broader `json.dumps` / `json.loads` compatibility. @overload async def send_json_to(self, data: dict[str, Any]) -> None: ... @overload async def send_json_to(self, data: Any) -> None: ... - async def receive_from(self, timeout: float = ...) -> str | bytes: ... - async def receive_json_from(self, timeout: float = ...) -> dict[str, Any]: ... - async def disconnect(self, code: int = ..., timeout: float = ...) -> None: ... + async def receive_json_from(self, timeout: float = 1) -> Any: ... + async def disconnect(self, code: int = 1000, timeout: float = 1) -> None: ... diff --git a/stubs/channels/channels/utils.pyi b/stubs/channels/channels/utils.pyi index b92c892b04e3..b220eca8cd38 100644 --- a/stubs/channels/channels/utils.pyi +++ b/stubs/channels/channels/utils.pyi @@ -8,6 +8,10 @@ def name_that_thing(thing: Any) -> str: ... async def await_many_dispatch( consumer_callables: list[Callable[[], Awaitable[ASGIReceiveCallable]]], dispatch: Callable[[dict[str, Any]], Awaitable[None]] ) -> None: ... + +# Defines a generic ASGI middleware protocol. +# All arguments are typed as `Any` to maximize compatibility with third-party ASGI middleware +# that may not strictly follow type conventions or use more specific signatures. @type_check_only class _MiddlewareProtocol(Protocol): def __init__(self, *args: Any, **kwargs: Any) -> None: ... diff --git a/stubs/channels/channels/worker.pyi b/stubs/channels/channels/worker.pyi index 08d979ac76a9..a20b5feec275 100644 --- a/stubs/channels/channels/worker.pyi +++ b/stubs/channels/channels/worker.pyi @@ -7,7 +7,7 @@ class Worker(StatelessServer): channel_layer: BaseChannelLayer def __init__( - self, application: _ChannelApplication, channels: list[str], channel_layer: BaseChannelLayer, max_applications: int = ... + self, application: _ChannelApplication, channels: list[str], channel_layer: BaseChannelLayer, max_applications: int = 1000 ) -> None: ... async def handle(self) -> None: ... async def listener(self, channel: str) -> None: ... From 3e856410e670cc083a5fd1cc8851240dd70e5052 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 10 Jul 2025 20:34:34 +0700 Subject: [PATCH 12/14] Enhance Django Channels type stubs with better annotations and parameter defaults --- stubs/channels/@tests/stubtest_allowlist.txt | 11 ++++--- stubs/channels/channels/apps.pyi | 4 ++- stubs/channels/channels/consumer.pyi | 2 ++ stubs/channels/channels/db.pyi | 12 ++++++- stubs/channels/channels/generic/websocket.pyi | 32 ++++++++----------- stubs/channels/channels/layers.pyi | 7 ++-- .../management/commands/runworker.pyi | 5 +-- stubs/channels/channels/sessions.pyi | 2 ++ .../channels/channels/testing/application.pyi | 9 ++++-- stubs/channels/channels/utils.pyi | 2 +- 10 files changed, 53 insertions(+), 33 deletions(-) diff --git a/stubs/channels/@tests/stubtest_allowlist.txt b/stubs/channels/@tests/stubtest_allowlist.txt index 6e1d93f95ec5..314adff482a3 100644 --- a/stubs/channels/@tests/stubtest_allowlist.txt +++ b/stubs/channels/@tests/stubtest_allowlist.txt @@ -1,7 +1,10 @@ -# channels.auth.UserLazyObject.DoesNotExist is not present at runtime -# channels.auth.UserLazyObject.MultipleObjectsReturned is not present at runtime -# channels.auth.UserLazyObject@AnnotatedWith is not present at runtime -channels.auth.UserLazyObject.* +# channels.auth.UserLazyObject metaclass is mismatch +channels.auth.UserLazyObject + +# these one need to be exclude due to mypy error: * is not present at runtime +channels.auth.UserLazyObject.DoesNotExist +channels.auth.UserLazyObject.MultipleObjectsReturned +channels.auth.UserLazyObject@AnnotatedWith # database_sync_to_async is implemented as a class instance but stubbed as a function # for better type inference when used as decorator/function diff --git a/stubs/channels/channels/apps.pyi b/stubs/channels/channels/apps.pyi index 33de39dc4c89..ad15a21b6961 100644 --- a/stubs/channels/channels/apps.pyi +++ b/stubs/channels/channels/apps.pyi @@ -1,5 +1,7 @@ +from typing import Final + from django.apps import AppConfig class ChannelsConfig(AppConfig): - name: str = "channels" + name: Final = "channels" verbose_name: str = "Channels" diff --git a/stubs/channels/channels/consumer.pyi b/stubs/channels/channels/consumer.pyi index 27a257921909..66a1ba0df89d 100644 --- a/stubs/channels/channels/consumer.pyi +++ b/stubs/channels/channels/consumer.pyi @@ -61,6 +61,8 @@ class AsyncConsumer: async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... async def dispatch(self, message: dict[str, Any]) -> None: ... async def send(self, message: dict[str, Any]) -> None: ... + + # initkwargs will be used to instantiate the consumer instance. @classmethod def as_asgi(cls, **initkwargs: Any) -> _ASGIApplicationProtocol: ... diff --git a/stubs/channels/channels/db.pyi b/stubs/channels/channels/db.pyi index 7e82ea89b5be..c0147a05b03d 100644 --- a/stubs/channels/channels/db.pyi +++ b/stubs/channels/channels/db.pyi @@ -1,3 +1,5 @@ +import asyncio +from _typeshed import OptExcInfo from asyncio import BaseEventLoop from collections.abc import Callable, Coroutine from concurrent.futures import ThreadPoolExecutor @@ -10,7 +12,15 @@ _P = ParamSpec("_P") _R = TypeVar("_R") class DatabaseSyncToAsync(SyncToAsync[_P, _R]): - def thread_handler(self, loop: BaseEventLoop, *args: Any, **kwargs: Any) -> Any: ... + def thread_handler( + self, + loop: BaseEventLoop, + exc_info: OptExcInfo, + task_context: list[asyncio.Task[Any]] | None, + func: Callable[_P, _R], + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _R: ... # We define `database_sync_to_async` as a function instead of assigning # `DatabaseSyncToAsync(...)` directly, to preserve both decorator and diff --git a/stubs/channels/channels/generic/websocket.pyi b/stubs/channels/channels/generic/websocket.pyi index 8dc44744fc80..8b2e38d1b017 100644 --- a/stubs/channels/channels/generic/websocket.pyi +++ b/stubs/channels/channels/generic/websocket.pyi @@ -1,18 +1,13 @@ +from _typeshed import Unused from typing import Any from asgiref.typing import WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveEvent -from channels.consumer import AsyncConsumer, SyncConsumer, _ChannelScope -from channels.layers import BaseChannelLayer +from channels.consumer import AsyncConsumer, SyncConsumer class WebsocketConsumer(SyncConsumer): groups: list[str] | None - scope: _ChannelScope - channel_name: str - channel_layer: BaseChannelLayer - channel_receive: Any - base_send: Any - def __init__(self, *args: Any, **kwargs: Any) -> None: ... + def __init__(self, *args: Unused, **kwargs: Unused) -> None: ... def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... def connect(self) -> None: ... def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ... @@ -27,22 +22,20 @@ class WebsocketConsumer(SyncConsumer): class JsonWebsocketConsumer(WebsocketConsumer): def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ... + # content is typed as Any to match json.loads() return type - JSON can represent + # various Python types (dict, list, str, int, float, bool, None) def receive_json(self, content: Any, **kwargs: Any) -> None: ... + # content is typed as Any to match json.dumps() input type - accepts any JSON-serializable object def send_json(self, content: Any, close: bool = False) -> None: ... @classmethod - def decode_json(cls, text_data: str) -> Any: ... + def decode_json(cls, text_data: str) -> Any: ... # Returns Any like json.loads() @classmethod - def encode_json(cls, content: Any) -> str: ... + def encode_json(cls, content: Any) -> str: ... # Accepts Any like json.dumps() class AsyncWebsocketConsumer(AsyncConsumer): groups: list[str] | None - scope: _ChannelScope - channel_name: str - channel_layer: BaseChannelLayer - channel_receive: Any - base_send: Any - def __init__(self, *args: Any, **kwargs: Any) -> None: ... + def __init__(self, *args: Unused, **kwargs: Unused) -> None: ... async def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... async def connect(self) -> None: ... async def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ... @@ -57,9 +50,12 @@ class AsyncWebsocketConsumer(AsyncConsumer): class AsyncJsonWebsocketConsumer(AsyncWebsocketConsumer): async def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ... + # content is typed as Any to match json.loads() return type - JSON can represent + # various Python types (dict, list, str, int, float, bool, None) async def receive_json(self, content: Any, **kwargs: Any) -> None: ... + # content is typed as Any to match json.dumps() input type - accepts any JSON-serializable object async def send_json(self, content: Any, close: bool = False) -> None: ... @classmethod - async def decode_json(cls, text_data: str) -> Any: ... + async def decode_json(cls, text_data: str) -> Any: ... # Returns Any like json.loads() @classmethod - async def encode_json(cls, content: Any) -> str: ... + async def encode_json(cls, content: Any) -> str: ... # Accepts Any like json.dumps() diff --git a/stubs/channels/channels/layers.pyi b/stubs/channels/channels/layers.pyi index f5ab05959be0..16cf0540bf62 100644 --- a/stubs/channels/channels/layers.pyi +++ b/stubs/channels/channels/layers.pyi @@ -34,15 +34,15 @@ class BaseChannelLayer: @overload def match_type_and_length(self, name: str) -> bool: ... @overload - def match_type_and_length(self, name: Any) -> bool: ... + def match_type_and_length(self, name: object) -> bool: ... @overload def require_valid_channel_name(self, name: str, receive: bool = False) -> bool: ... @overload - def require_valid_channel_name(self, name: Any, receive: bool = False) -> bool: ... + def require_valid_channel_name(self, name: object, receive: bool = False) -> bool: ... @overload def require_valid_group_name(self, name: str) -> bool: ... @overload - def require_valid_group_name(self, name: Any) -> bool: ... + def require_valid_group_name(self, name: object) -> bool: ... @overload def valid_channel_names(self, names: list[str], receive: bool = False) -> bool: ... @overload @@ -73,7 +73,6 @@ class InMemoryChannelLayer(BaseChannelLayer): group_expiry: int = 86400, capacity: int = 100, channel_capacity: _ChannelCapacityDict | None = ..., - **kwargs: Any, ) -> None: ... extensions: list[str] diff --git a/stubs/channels/channels/management/commands/runworker.pyi b/stubs/channels/channels/management/commands/runworker.pyi index b202effec2dd..533b94cea88d 100644 --- a/stubs/channels/channels/management/commands/runworker.pyi +++ b/stubs/channels/channels/management/commands/runworker.pyi @@ -1,6 +1,7 @@ import logging +from _typeshed import Unused from argparse import ArgumentParser -from typing import Any, TypedDict, type_check_only +from typing import TypedDict, type_check_only from channels.layers import BaseChannelLayer from channels.worker import Worker @@ -21,4 +22,4 @@ class Command(BaseCommand): channel_layer: BaseChannelLayer def add_arguments(self, parser: ArgumentParser) -> None: ... - def handle(self, *args: Any, **options: _RunWorkerCommandOption) -> None: ... + def handle(self, *args: Unused, **options: _RunWorkerCommandOption) -> None: ... diff --git a/stubs/channels/channels/sessions.pyi b/stubs/channels/channels/sessions.pyi index fcba517df584..04a4eb729535 100644 --- a/stubs/channels/channels/sessions.pyi +++ b/stubs/channels/channels/sessions.pyi @@ -11,6 +11,8 @@ class CookieMiddleware: inner: _ChannelApplication def __init__(self, inner: _ChannelApplication) -> None: ... + + # Returns the same type as the provided _ChannelApplication. async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Any: ... @classmethod def set_cookie( diff --git a/stubs/channels/channels/testing/application.pyi b/stubs/channels/channels/testing/application.pyi index 1984a28e32ff..db3f567c1d61 100644 --- a/stubs/channels/channels/testing/application.pyi +++ b/stubs/channels/channels/testing/application.pyi @@ -5,8 +5,13 @@ from asgiref.testing import ApplicationCommunicator as BaseApplicationCommunicat def no_op() -> None: ... class ApplicationCommunicator(BaseApplicationCommunicator): - async def send_input(self, message: Any) -> None: ... - async def receive_output(self, timeout: float = 1) -> Any: ... + # ASGI messages are dictionaries with a "type" key and protocol-specific fields. + # Dictionary values can be strings, bytes, lists, or other types depending on the protocol: + # - HTTP: {"type": "http.request", "body": b"request data", "headers": [...], ...} + # - WebSocket: {"type": "websocket.receive", "bytes": b"binary data"} or {"text": "string"} + # - Custom protocols: Application-specific message dictionaries + async def send_input(self, message: dict[str, Any]) -> None: ... + async def receive_output(self, timeout: float = 1) -> dict[str, Any]: ... # The following methods are not present in the original source code, # but are commonly used in practice. Since the base package doesn't diff --git a/stubs/channels/channels/utils.pyi b/stubs/channels/channels/utils.pyi index b220eca8cd38..0e0818abbbb6 100644 --- a/stubs/channels/channels/utils.pyi +++ b/stubs/channels/channels/utils.pyi @@ -4,7 +4,7 @@ from typing_extensions import TypeAlias from asgiref.typing import ASGIApplication, ASGIReceiveCallable -def name_that_thing(thing: Any) -> str: ... +def name_that_thing(thing: object) -> str: ... async def await_many_dispatch( consumer_callables: list[Callable[[], Awaitable[ASGIReceiveCallable]]], dispatch: Callable[[dict[str, Any]], Awaitable[None]] ) -> None: ... From 67f447be11a0eaeb48f2e914a7804190d2a0c6fb Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sat, 12 Jul 2025 00:25:06 +0700 Subject: [PATCH 13/14] Correct websocket consumer groups type --- stubs/channels/@tests/stubtest_allowlist.txt | 5 +++++ stubs/channels/channels/generic/websocket.pyi | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/stubs/channels/@tests/stubtest_allowlist.txt b/stubs/channels/@tests/stubtest_allowlist.txt index 314adff482a3..371428961047 100644 --- a/stubs/channels/@tests/stubtest_allowlist.txt +++ b/stubs/channels/@tests/stubtest_allowlist.txt @@ -9,3 +9,8 @@ channels.auth.UserLazyObject@AnnotatedWith # database_sync_to_async is implemented as a class instance but stubbed as a function # for better type inference when used as decorator/function channels.db.database_sync_to_async + + +# Due to groups type correction raise runtime value, we need to ignore them +channels.generic.websocket.WebsocketConsumer.groups +channels.generic.websocket.AsyncWebsocketConsumer.groups diff --git a/stubs/channels/channels/generic/websocket.pyi b/stubs/channels/channels/generic/websocket.pyi index 8b2e38d1b017..f19d406e1e5d 100644 --- a/stubs/channels/channels/generic/websocket.pyi +++ b/stubs/channels/channels/generic/websocket.pyi @@ -5,7 +5,7 @@ from asgiref.typing import WebSocketConnectEvent, WebSocketDisconnectEvent, WebS from channels.consumer import AsyncConsumer, SyncConsumer class WebsocketConsumer(SyncConsumer): - groups: list[str] | None + groups: list[str] def __init__(self, *args: Unused, **kwargs: Unused) -> None: ... def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... @@ -33,7 +33,7 @@ class JsonWebsocketConsumer(WebsocketConsumer): def encode_json(cls, content: Any) -> str: ... # Accepts Any like json.dumps() class AsyncWebsocketConsumer(AsyncConsumer): - groups: list[str] | None + groups: list[str] def __init__(self, *args: Unused, **kwargs: Unused) -> None: ... async def websocket_connect(self, message: WebSocketConnectEvent) -> None: ... From daf646bf700a9f5adb702b913cf8acb673615c32 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 11 Jul 2025 19:41:50 +0200 Subject: [PATCH 14/14] Update stubs/channels/@tests/stubtest_allowlist.txt Co-authored-by: Brian Schubert --- stubs/channels/@tests/stubtest_allowlist.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stubs/channels/@tests/stubtest_allowlist.txt b/stubs/channels/@tests/stubtest_allowlist.txt index 371428961047..c5c5761b73fe 100644 --- a/stubs/channels/@tests/stubtest_allowlist.txt +++ b/stubs/channels/@tests/stubtest_allowlist.txt @@ -10,7 +10,6 @@ channels.auth.UserLazyObject@AnnotatedWith # for better type inference when used as decorator/function channels.db.database_sync_to_async - -# Due to groups type correction raise runtime value, we need to ignore them +# Set to None on class, but initialized to non-None value in __init__ channels.generic.websocket.WebsocketConsumer.groups channels.generic.websocket.AsyncWebsocketConsumer.groups