Skip to content

Commit 3e85641

Browse files
committed
Enhance Django Channels type stubs with better annotations and parameter defaults
1 parent 4f80ba1 commit 3e85641

File tree

10 files changed

+53
-33
lines changed

10 files changed

+53
-33
lines changed

stubs/channels/@tests/stubtest_allowlist.txt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
# channels.auth.UserLazyObject.DoesNotExist is not present at runtime
2-
# channels.auth.UserLazyObject.MultipleObjectsReturned is not present at runtime
3-
# channels.auth.UserLazyObject@AnnotatedWith is not present at runtime
4-
channels.auth.UserLazyObject.*
1+
# channels.auth.UserLazyObject metaclass is mismatch
2+
channels.auth.UserLazyObject
3+
4+
# these one need to be exclude due to mypy error: * is not present at runtime
5+
channels.auth.UserLazyObject.DoesNotExist
6+
channels.auth.UserLazyObject.MultipleObjectsReturned
7+
channels.auth.UserLazyObject@AnnotatedWith
58

69
# database_sync_to_async is implemented as a class instance but stubbed as a function
710
# for better type inference when used as decorator/function

stubs/channels/channels/apps.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from typing import Final
2+
13
from django.apps import AppConfig
24

35
class ChannelsConfig(AppConfig):
4-
name: str = "channels"
6+
name: Final = "channels"
57
verbose_name: str = "Channels"

stubs/channels/channels/consumer.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class AsyncConsumer:
6161
async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ...
6262
async def dispatch(self, message: dict[str, Any]) -> None: ...
6363
async def send(self, message: dict[str, Any]) -> None: ...
64+
65+
# initkwargs will be used to instantiate the consumer instance.
6466
@classmethod
6567
def as_asgi(cls, **initkwargs: Any) -> _ASGIApplicationProtocol: ...
6668

stubs/channels/channels/db.pyi

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
from _typeshed import OptExcInfo
13
from asyncio import BaseEventLoop
24
from collections.abc import Callable, Coroutine
35
from concurrent.futures import ThreadPoolExecutor
@@ -10,7 +12,15 @@ _P = ParamSpec("_P")
1012
_R = TypeVar("_R")
1113

1214
class DatabaseSyncToAsync(SyncToAsync[_P, _R]):
13-
def thread_handler(self, loop: BaseEventLoop, *args: Any, **kwargs: Any) -> Any: ...
15+
def thread_handler(
16+
self,
17+
loop: BaseEventLoop,
18+
exc_info: OptExcInfo,
19+
task_context: list[asyncio.Task[Any]] | None,
20+
func: Callable[_P, _R],
21+
*args: _P.args,
22+
**kwargs: _P.kwargs,
23+
) -> _R: ...
1424

1525
# We define `database_sync_to_async` as a function instead of assigning
1626
# `DatabaseSyncToAsync(...)` directly, to preserve both decorator and
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1+
from _typeshed import Unused
12
from typing import Any
23

34
from asgiref.typing import WebSocketConnectEvent, WebSocketDisconnectEvent, WebSocketReceiveEvent
4-
from channels.consumer import AsyncConsumer, SyncConsumer, _ChannelScope
5-
from channels.layers import BaseChannelLayer
5+
from channels.consumer import AsyncConsumer, SyncConsumer
66

77
class WebsocketConsumer(SyncConsumer):
88
groups: list[str] | None
9-
scope: _ChannelScope
10-
channel_name: str
11-
channel_layer: BaseChannelLayer
12-
channel_receive: Any
13-
base_send: Any
149

15-
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
10+
def __init__(self, *args: Unused, **kwargs: Unused) -> None: ...
1611
def websocket_connect(self, message: WebSocketConnectEvent) -> None: ...
1712
def connect(self) -> None: ...
1813
def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ...
@@ -27,22 +22,20 @@ class WebsocketConsumer(SyncConsumer):
2722

2823
class JsonWebsocketConsumer(WebsocketConsumer):
2924
def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ...
25+
# content is typed as Any to match json.loads() return type - JSON can represent
26+
# various Python types (dict, list, str, int, float, bool, None)
3027
def receive_json(self, content: Any, **kwargs: Any) -> None: ...
28+
# content is typed as Any to match json.dumps() input type - accepts any JSON-serializable object
3129
def send_json(self, content: Any, close: bool = False) -> None: ...
3230
@classmethod
33-
def decode_json(cls, text_data: str) -> Any: ...
31+
def decode_json(cls, text_data: str) -> Any: ... # Returns Any like json.loads()
3432
@classmethod
35-
def encode_json(cls, content: Any) -> str: ...
33+
def encode_json(cls, content: Any) -> str: ... # Accepts Any like json.dumps()
3634

3735
class AsyncWebsocketConsumer(AsyncConsumer):
3836
groups: list[str] | None
39-
scope: _ChannelScope
40-
channel_name: str
41-
channel_layer: BaseChannelLayer
42-
channel_receive: Any
43-
base_send: Any
4437

45-
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
38+
def __init__(self, *args: Unused, **kwargs: Unused) -> None: ...
4639
async def websocket_connect(self, message: WebSocketConnectEvent) -> None: ...
4740
async def connect(self) -> None: ...
4841
async def accept(self, subprotocol: str | None = None, headers: list[tuple[str, str]] | None = None) -> None: ...
@@ -57,9 +50,12 @@ class AsyncWebsocketConsumer(AsyncConsumer):
5750

5851
class AsyncJsonWebsocketConsumer(AsyncWebsocketConsumer):
5952
async def receive(self, text_data: str | None = None, bytes_data: bytes | None = None, **kwargs: Any) -> None: ...
53+
# content is typed as Any to match json.loads() return type - JSON can represent
54+
# various Python types (dict, list, str, int, float, bool, None)
6055
async def receive_json(self, content: Any, **kwargs: Any) -> None: ...
56+
# content is typed as Any to match json.dumps() input type - accepts any JSON-serializable object
6157
async def send_json(self, content: Any, close: bool = False) -> None: ...
6258
@classmethod
63-
async def decode_json(cls, text_data: str) -> Any: ...
59+
async def decode_json(cls, text_data: str) -> Any: ... # Returns Any like json.loads()
6460
@classmethod
65-
async def encode_json(cls, content: Any) -> str: ...
61+
async def encode_json(cls, content: Any) -> str: ... # Accepts Any like json.dumps()

stubs/channels/channels/layers.pyi

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ class BaseChannelLayer:
3434
@overload
3535
def match_type_and_length(self, name: str) -> bool: ...
3636
@overload
37-
def match_type_and_length(self, name: Any) -> bool: ...
37+
def match_type_and_length(self, name: object) -> bool: ...
3838
@overload
3939
def require_valid_channel_name(self, name: str, receive: bool = False) -> bool: ...
4040
@overload
41-
def require_valid_channel_name(self, name: Any, receive: bool = False) -> bool: ...
41+
def require_valid_channel_name(self, name: object, receive: bool = False) -> bool: ...
4242
@overload
4343
def require_valid_group_name(self, name: str) -> bool: ...
4444
@overload
45-
def require_valid_group_name(self, name: Any) -> bool: ...
45+
def require_valid_group_name(self, name: object) -> bool: ...
4646
@overload
4747
def valid_channel_names(self, names: list[str], receive: bool = False) -> bool: ...
4848
@overload
@@ -73,7 +73,6 @@ class InMemoryChannelLayer(BaseChannelLayer):
7373
group_expiry: int = 86400,
7474
capacity: int = 100,
7575
channel_capacity: _ChannelCapacityDict | None = ...,
76-
**kwargs: Any,
7776
) -> None: ...
7877

7978
extensions: list[str]

stubs/channels/channels/management/commands/runworker.pyi

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2+
from _typeshed import Unused
23
from argparse import ArgumentParser
3-
from typing import Any, TypedDict, type_check_only
4+
from typing import TypedDict, type_check_only
45

56
from channels.layers import BaseChannelLayer
67
from channels.worker import Worker
@@ -21,4 +22,4 @@ class Command(BaseCommand):
2122
channel_layer: BaseChannelLayer
2223

2324
def add_arguments(self, parser: ArgumentParser) -> None: ...
24-
def handle(self, *args: Any, **options: _RunWorkerCommandOption) -> None: ...
25+
def handle(self, *args: Unused, **options: _RunWorkerCommandOption) -> None: ...

stubs/channels/channels/sessions.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class CookieMiddleware:
1111
inner: _ChannelApplication
1212

1313
def __init__(self, inner: _ChannelApplication) -> None: ...
14+
15+
# Returns the same type as the provided _ChannelApplication.
1416
async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Any: ...
1517
@classmethod
1618
def set_cookie(

stubs/channels/channels/testing/application.pyi

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ from asgiref.testing import ApplicationCommunicator as BaseApplicationCommunicat
55
def no_op() -> None: ...
66

77
class ApplicationCommunicator(BaseApplicationCommunicator):
8-
async def send_input(self, message: Any) -> None: ...
9-
async def receive_output(self, timeout: float = 1) -> Any: ...
8+
# ASGI messages are dictionaries with a "type" key and protocol-specific fields.
9+
# Dictionary values can be strings, bytes, lists, or other types depending on the protocol:
10+
# - HTTP: {"type": "http.request", "body": b"request data", "headers": [...], ...}
11+
# - WebSocket: {"type": "websocket.receive", "bytes": b"binary data"} or {"text": "string"}
12+
# - Custom protocols: Application-specific message dictionaries
13+
async def send_input(self, message: dict[str, Any]) -> None: ...
14+
async def receive_output(self, timeout: float = 1) -> dict[str, Any]: ...
1015

1116
# The following methods are not present in the original source code,
1217
# but are commonly used in practice. Since the base package doesn't

stubs/channels/channels/utils.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from typing_extensions import TypeAlias
44

55
from asgiref.typing import ASGIApplication, ASGIReceiveCallable
66

7-
def name_that_thing(thing: Any) -> str: ...
7+
def name_that_thing(thing: object) -> str: ...
88
async def await_many_dispatch(
99
consumer_callables: list[Callable[[], Awaitable[ASGIReceiveCallable]]], dispatch: Callable[[dict[str, Any]], Awaitable[None]]
1010
) -> None: ...

0 commit comments

Comments
 (0)