Skip to content

Commit 844e132

Browse files
Django channels stubs (#13939)
1 parent d5af6be commit 844e132

29 files changed

+688
-0
lines changed

pyrightconfig.stricter.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"stubs/boltons",
3232
"stubs/braintree",
3333
"stubs/cffi",
34+
"stubs/channels",
3435
"stubs/dateparser",
3536
"stubs/defusedxml",
3637
"stubs/docker",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SECRET_KEY = "1"
2+
3+
INSTALLED_APPS = (
4+
"django.contrib.contenttypes",
5+
"django.contrib.sites",
6+
"django.contrib.sessions",
7+
"django.contrib.messages",
8+
"django.contrib.admin.apps.SimpleAdminConfig",
9+
"django.contrib.staticfiles",
10+
"django.contrib.auth",
11+
"channels",
12+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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
8+
9+
# database_sync_to_async is implemented as a class instance but stubbed as a function
10+
# for better type inference when used as decorator/function
11+
channels.db.database_sync_to_async
12+
13+
# Set to None on class, but initialized to non-None value in __init__
14+
channels.generic.websocket.WebsocketConsumer.groups
15+
channels.generic.websocket.AsyncWebsocketConsumer.groups

stubs/channels/METADATA.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version = "4.2.*"
2+
upstream_repository = "https://github.com/django/channels"
3+
requires = ["django-stubs>=4.2,<5.3", "asgiref"]
4+
5+
[tool.stubtest]
6+
mypy_plugins = ['mypy_django_plugin.main']
7+
mypy_plugins_config = {"django-stubs" = {"django_settings_module" = "@tests.django_settings"}}
8+
stubtest_requirements = ["daphne"]

stubs/channels/channels/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from typing import Final
2+
3+
__version__: Final[str]
4+
DEFAULT_CHANNEL_LAYER: Final[str]

stubs/channels/channels/apps.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import Final
2+
3+
from django.apps import AppConfig
4+
5+
class ChannelsConfig(AppConfig):
6+
name: Final = "channels"
7+
verbose_name: str = "Channels"

stubs/channels/channels/auth.pyi

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from asgiref.typing import ASGIReceiveCallable, ASGISendCallable
2+
from channels.middleware import BaseMiddleware
3+
from django.contrib.auth.backends import BaseBackend
4+
from django.contrib.auth.base_user import AbstractBaseUser
5+
from django.contrib.auth.models import AnonymousUser
6+
from django.utils.functional import LazyObject
7+
8+
from .consumer import _ChannelScope
9+
from .utils import _ChannelApplication
10+
11+
async def get_user(scope: _ChannelScope) -> AbstractBaseUser | AnonymousUser: ...
12+
async def login(scope: _ChannelScope, user: AbstractBaseUser, backend: BaseBackend | None = None) -> None: ...
13+
async def logout(scope: _ChannelScope) -> None: ...
14+
15+
# Inherits AbstractBaseUser to improve autocomplete and show this is a lazy proxy for a user.
16+
# At runtime, it's just a LazyObject that wraps the actual user instance.
17+
class UserLazyObject(AbstractBaseUser, LazyObject): ...
18+
19+
class AuthMiddleware(BaseMiddleware):
20+
def populate_scope(self, scope: _ChannelScope) -> None: ...
21+
async def resolve_scope(self, scope: _ChannelScope) -> None: ...
22+
async def __call__(
23+
self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable
24+
) -> _ChannelApplication: ...
25+
26+
def AuthMiddlewareStack(inner: _ChannelApplication) -> _ChannelApplication: ...

stubs/channels/channels/consumer.pyi

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from collections.abc import Awaitable
2+
from typing import Any, ClassVar, Protocol, TypedDict, type_check_only
3+
4+
from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, Scope, WebSocketScope
5+
from channels.auth import UserLazyObject
6+
from channels.db import database_sync_to_async
7+
from channels.layers import BaseChannelLayer
8+
from django.contrib.sessions.backends.base import SessionBase
9+
from django.utils.functional import LazyObject
10+
11+
# _LazySession is a LazyObject that wraps a SessionBase instance.
12+
# We subclass both for type checking purposes to expose SessionBase attributes,
13+
# and suppress mypy's "misc" error with `# type: ignore[misc]`.
14+
@type_check_only
15+
class _LazySession(SessionBase, LazyObject): # type: ignore[misc]
16+
_wrapped: SessionBase
17+
18+
@type_check_only
19+
class _URLRoute(TypedDict):
20+
# Values extracted from Django's URLPattern matching,
21+
# passed through ASGI scope routing.
22+
# `args` and `kwargs` are the result of pattern matching against the URL path.
23+
args: tuple[Any, ...]
24+
kwargs: dict[str, Any]
25+
26+
# Channel Scope definition
27+
@type_check_only
28+
class _ChannelScope(WebSocketScope, total=False):
29+
# Channels specific
30+
channel: str
31+
url_route: _URLRoute
32+
path_remaining: str
33+
34+
# Auth specific
35+
cookies: dict[str, str]
36+
session: _LazySession
37+
user: UserLazyObject | None
38+
39+
# Accepts any ASGI message dict with a required "type" key (str),
40+
# but allows additional arbitrary keys for flexibility.
41+
def get_handler_name(message: dict[str, Any]) -> str: ...
42+
@type_check_only
43+
class _ASGIApplicationProtocol(Protocol):
44+
consumer_class: AsyncConsumer
45+
46+
# Accepts any initialization kwargs passed to the consumer class.
47+
# Typed as `Any` to allow flexibility in subclass-specific arguments.
48+
consumer_initkwargs: Any
49+
50+
def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> Awaitable[None]: ...
51+
52+
class AsyncConsumer:
53+
channel_layer_alias: ClassVar[str]
54+
55+
scope: _ChannelScope
56+
channel_layer: BaseChannelLayer
57+
channel_name: str
58+
channel_receive: ASGIReceiveCallable
59+
base_send: ASGISendCallable
60+
61+
async def __call__(self, scope: _ChannelScope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ...
62+
async def dispatch(self, message: dict[str, Any]) -> None: ...
63+
async def send(self, message: dict[str, Any]) -> None: ...
64+
65+
# initkwargs will be used to instantiate the consumer instance.
66+
@classmethod
67+
def as_asgi(cls, **initkwargs: Any) -> _ASGIApplicationProtocol: ...
68+
69+
class SyncConsumer(AsyncConsumer):
70+
71+
# Since we're overriding asynchronous methods with synchronous ones,
72+
# we need to use `# type: ignore[override]` to suppress mypy errors.
73+
@database_sync_to_async
74+
def dispatch(self, message: dict[str, Any]) -> None: ... # type: ignore[override]
75+
def send(self, message: dict[str, Any]) -> None: ... # type: ignore[override]

stubs/channels/channels/db.pyi

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import asyncio
2+
from _typeshed import OptExcInfo
3+
from asyncio import BaseEventLoop
4+
from collections.abc import Callable, Coroutine
5+
from concurrent.futures import ThreadPoolExecutor
6+
from typing import Any, TypeVar
7+
from typing_extensions import ParamSpec
8+
9+
from asgiref.sync import SyncToAsync
10+
11+
_P = ParamSpec("_P")
12+
_R = TypeVar("_R")
13+
14+
class DatabaseSyncToAsync(SyncToAsync[_P, _R]):
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: ...
24+
25+
# We define `database_sync_to_async` as a function instead of assigning
26+
# `DatabaseSyncToAsync(...)` directly, to preserve both decorator and
27+
# higher-order function behavior with correct type hints.
28+
# A direct assignment would result in incorrect type inference for the wrapped function.
29+
def database_sync_to_async(
30+
func: Callable[_P, _R], thread_sensitive: bool = True, executor: ThreadPoolExecutor | None = None
31+
) -> Callable[_P, Coroutine[Any, Any, _R]]: ...
32+
async def aclose_old_connections() -> None: ...
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class RequestAborted(Exception): ...
2+
class RequestTimeout(RequestAborted): ...
3+
class InvalidChannelLayerError(ValueError): ...
4+
class AcceptConnection(Exception): ...
5+
class DenyConnection(Exception): ...
6+
class ChannelFull(Exception): ...
7+
class MessageTooLarge(Exception): ...
8+
class StopConsumer(Exception): ...

0 commit comments

Comments
 (0)