Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Django channels stubs #13939

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
Loading
from
Next Next commit
Django channels stubs
  • Loading branch information
huynguyengl99 committed May 12, 2025
commit f6f330bc52173b8f9192a07f8c085db4bb692cc4
1 change: 1 addition & 0 deletions 1 pyrightconfig.stricter.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"stubs/braintree",
"stubs/caldav",
"stubs/cffi",
"stubs/channels",
"stubs/click-web",
"stubs/corus",
"stubs/dateparser",
Expand Down
8 changes: 8 additions & 0 deletions 8 stubs/channels/METADATA.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version = "4.*"
upstream_repository = "https://github.com/django/channels"
requires = ["django-stubs", "asgiref"]
huynguyengl99 marked this conversation as resolved.
Show resolved Hide resolved
requires_python = ">=3.10"

[tool.stubtest]
skip = true # due to the need of django mypy plugin config, it should be skipped.
huynguyengl99 marked this conversation as resolved.
Show resolved Hide resolved
stubtest_requirements = ["daphne"]
2 changes: 2 additions & 0 deletions 2 stubs/channels/channels/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__version__: str
DEFAULT_CHANNEL_LAYER: str
6 changes: 6 additions & 0 deletions 6 stubs/channels/channels/apps.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig
from django.utils.functional import _StrOrPromise

class ChannelsConfig(AppConfig):
name: str = ...
verbose_name: _StrOrPromise = ...
32 changes: 32 additions & 0 deletions 32 stubs/channels/channels/auth.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just turn this into async def? Or will it trigger an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will work for the auth.pyi. However, there is one thing I have encountered in another file: stubs/channels/channels/consumer.pyi. If I migrate the dispatch function from being annotated by database_sync_to_async to async def, I face this error in the stub test:

error: channels.consumer.SyncConsumer.dispatch is not a function
Stub: in file typeshed/stubs/channels/channels/consumer.pyi:55
def (self: channels.consumer.SyncConsumer, message: builtins.dict[builtins.str, Any]) -> typing.Coroutine[Any, Any, None]
Runtime: at line 118
async def (self, message)

If I use database_sync_to_async here, it's ok. I don't know why there is this difference, so I chose to use database_sync_to_async. But I'm willing to migrate to async def and put dispatch in allow_list.txt if you think async def is better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have migrated all functions in auth.pyi to use async def but kept dispatch in consumer.pyi using database_sync_to_async. Please let me know if this approach is correct, thank you.

@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: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this protected member? please, add a comment. We generally don't included protected names, until they are special / required.

Copy link
Contributor Author

@huynguyengl99 huynguyengl99 May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, wasn't aware of that convention. I'll remove the protected functions that are purely internal. Thanks for pointing this out!

I also remove these protected methods from ChannelLayerManager that appear to be internal-only:

class ChannelLayerManager:
    # ... other methods ...
    def _reset_backends(self, setting: str, **kwargs: Any) -> None: ...
    def _make_backend(self, name: str, config: dict[str, Any]) -> BaseChannelLayer: ...
```"


class UserLazyObject(AbstractBaseUser, LazyObject):
def _setup(self) -> None: ...
huynguyengl99 marked this conversation as resolved.
Show resolved Hide resolved

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: ...
54 changes: 54 additions & 0 deletions 54 stubs/channels/channels/consumer.pyi
Original file line number Diff line number Diff line change
@@ -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
huynguyengl99 marked this conversation as resolved.
Show resolved Hide resolved
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]
15 changes: 15 additions & 0 deletions 15 stubs/channels/channels/db.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
8 changes: 8 additions & 0 deletions 8 stubs/channels/channels/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -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): ...
Empty file.
19 changes: 19 additions & 0 deletions 19 stubs/channels/channels/generic/http.pyi
Original file line number Diff line number Diff line change
@@ -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]
64 changes: 64 additions & 0 deletions 64 stubs/channels/channels/generic/websocket.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
96 changes: 96 additions & 0 deletions 96 stubs/channels/channels/layers.pyi
Original file line number Diff line number Diff line change
@@ -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
Empty file.
Empty file.
20 changes: 20 additions & 0 deletions 20 stubs/channels/channels/management/commands/runworker.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
12 changes: 12 additions & 0 deletions 12 stubs/channels/channels/middleware.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
31 changes: 31 additions & 0 deletions 31 stubs/channels/channels/routing.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Empty file.
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.