diff --git a/django-stubs/core/cache/__init__.pyi b/django-stubs/core/cache/__init__.pyi index f7d0b294b..8249858b2 100644 --- a/django-stubs/core/cache/__init__.pyi +++ b/django-stubs/core/cache/__init__.pyi @@ -1,23 +1,35 @@ -from collections import OrderedDict -from collections.abc import Callable from typing import Any +from django.utils.connection import BaseConnectionHandler +from typing_extensions import override + from .backends.base import BaseCache as BaseCache from .backends.base import CacheKeyWarning as CacheKeyWarning from .backends.base import InvalidCacheBackendError as InvalidCacheBackendError +from .backends.base import InvalidCacheKey as InvalidCacheKey DEFAULT_CACHE_ALIAS: str -class CacheHandler: - def __init__(self) -> None: ... - def __getitem__(self, alias: str) -> BaseCache: ... - def all(self) -> Any: ... +class CacheHandler(BaseConnectionHandler[BaseCache]): + settings_name: str + exception_class: type[Exception] + @override + def create_connection(self, alias: str) -> BaseCache: ... + @override + def all(self, initialized_only: bool = False) -> list[BaseCache]: ... -class DefaultCacheProxy: - def __getattr__(self, name: str) -> Callable[..., Any] | dict[str, float] | OrderedDict[Any, Any] | int: ... - def __setattr__(self, name: str, value: Callable[..., Any]) -> None: ... - def __delattr__(self, name: Any) -> Any: ... - def __contains__(self, key: str) -> bool: ... +def close_caches(**kwargs: Any) -> None: ... -cache: BaseCache caches: CacheHandler +# Actually ConnectionProxy, but quacks exactly like BaseCache, it's not worth distinguishing the two. +cache: BaseCache + +__all__ = [ + "DEFAULT_CACHE_ALIAS", + "BaseCache", + "CacheKeyWarning", + "InvalidCacheBackendError", + "InvalidCacheKey", + "cache", + "caches", +] diff --git a/django-stubs/core/cache/backends/base.pyi b/django-stubs/core/cache/backends/base.pyi index 60387130f..c87be3333 100644 --- a/django-stubs/core/cache/backends/base.pyi +++ b/django-stubs/core/cache/backends/base.pyi @@ -1,93 +1,72 @@ -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Iterator +from re import Pattern from typing import Any from django.core.exceptions import ImproperlyConfigured class InvalidCacheBackendError(ImproperlyConfigured): ... class CacheKeyWarning(RuntimeWarning): ... +class InvalidCacheKey(ValueError): ... -DEFAULT_TIMEOUT: int +DEFAULT_TIMEOUT: Any MEMCACHE_MAX_KEY_LENGTH: int -def default_key_func(key: str, key_prefix: str, version: Any) -> str: ... -def get_key_func(key_func: Callable[..., Any] | str | None) -> Callable[..., Any]: ... +def default_key_func(key: Any, key_prefix: str, version: Any) -> str: ... +def get_key_func(key_func: Callable | str | None) -> Callable: ... class BaseCache: - default_timeout: int = ... - key_prefix: str = ... - version: int = ... - key_func: Callable[..., Any] = ... + _missing_key: object + default_timeout: float | None + _max_entries: int + _cull_frequency: int + key_prefix: str + version: int + key_func: Callable def __init__(self, params: dict[str, Any]) -> None: ... - def get_backend_timeout(self, timeout: int | None = ...) -> float | None: ... - def make_key(self, key: str, version: int | None = ...) -> str: ... - def add( - self, - key: str, - value: Any, - timeout: int | None = ..., - version: int | None = ..., - ) -> bool: ... - async def aadd( - self, - key: str, - value: Any, - timeout: int | None = ..., - version: int | None = ..., - ) -> bool: ... - def get(self, key: str, default: Any | None = ..., version: int | None = ...) -> Any: ... - async def aget(self, key: str, default: Any | None = ..., version: int | None = ...) -> Any: ... - def set( - self, - key: str, - value: Any, - timeout: int | None = ..., - version: int | None = ..., - ) -> None: ... - async def aset( - self, - key: str, - value: Any, - timeout: int | None = ..., - version: int | None = ..., - ) -> None: ... - def touch(self, key: str, timeout: int | None = ..., version: int | None = ...) -> bool: ... - def delete(self, key: str, version: int | None = ...) -> None: ... - def get_many(self, keys: Iterable[str], version: int | None = ...) -> dict[str, int | str]: ... - async def aget_many(self, keys: Iterable[str], version: int | None = ...) -> dict[str, int | str]: ... + def get_backend_timeout(self, timeout: float | None = ...) -> float | None: ... + def make_key(self, key: Any, version: int | None = None) -> str: ... + def validate_key(self, key: Any) -> None: ... + def make_and_validate_key(self, key: Any, version: int | None = None) -> str: ... + def add(self, key: Any, value: Any, timeout: float | None = ..., version: int | None = None) -> bool: ... + async def aadd(self, key: Any, value: Any, timeout: float | None = ..., version: int | None = None) -> bool: ... + def get(self, key: Any, default: Any | None = None, version: int | None = None) -> Any: ... + async def aget(self, key: Any, default: Any | None = None, version: int | None = None) -> Any: ... + def set(self, key: Any, value: Any, timeout: float | None = ..., version: int | None = None) -> None: ... + async def aset(self, key: Any, value: Any, timeout: float | None = ..., version: int | None = None) -> None: ... + def touch(self, key: Any, timeout: float | None = ..., version: int | None = None) -> bool: ... + async def atouch(self, key: Any, timeout: float | None = ..., version: int | None = None) -> bool: ... + def delete(self, key: Any, version: int | None = None) -> bool: ... + async def adelete(self, key: Any, version: int | None = None) -> bool: ... + def get_many(self, keys: Iterable[Any], version: int | None = None) -> dict[Any, Any]: ... + async def aget_many(self, keys: Iterable[Any], version: int | None = None) -> dict[Any, Any]: ... def get_or_set( - self, - key: str, - default: Any | None, - timeout: int | None = ..., - version: int | None = ..., + self, key: Any, default: Any | None, timeout: float | None = ..., version: int | None = None ) -> Any | None: ... async def aget_or_set( - self, - key: str, - default: Any | None, - timeout: int | None = ..., - version: int | None = ..., + self, key: Any, default: Any | None, timeout: float | None = ..., version: int | None = None ) -> Any | None: ... - def has_key(self, key: str, version: int | None = ...) -> bool: ... - def incr(self, key: str, delta: int = ..., version: int | None = ...) -> int: ... - def decr(self, key: str, delta: int = ..., version: int | None = ...) -> int: ... - def __contains__(self, key: str) -> bool: ... - def set_many( - self, - data: dict[str, Any], - timeout: int | None = ..., - version: int | None = ..., - ) -> list[Any]: ... + def has_key(self, key: Any, version: int | None = None) -> bool: ... + async def ahas_key(self, key: Any, version: int | None = None) -> bool: ... + def incr(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + async def aincr(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + def decr(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + async def adecr(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + def __contains__(self, key: Any) -> bool: ... + def set_many(self, data: dict[Any, Any], timeout: float | None = ..., version: int | None = None) -> list[Any]: ... async def aset_many( - self, - data: dict[str, Any], - timeout: int | None = ..., - version: int | None = ..., + self, data: dict[Any, Any], timeout: float | None = ..., version: int | None = None ) -> list[Any]: ... - def delete_many(self, keys: Iterable[str], version: int | None = ...) -> None: ... - async def adelete_many(self, keys: Iterable[str], version: int | None = ...) -> None: ... + def delete_many(self, keys: Iterable[Any], version: int | None = None) -> None: ... + async def adelete_many(self, keys: Iterable[Any], version: int | None = None) -> None: ... def clear(self) -> None: ... - def validate_key(self, key: str) -> None: ... - def incr_version(self, key: str, delta: int = ..., version: int | None = ...) -> int: ... - def decr_version(self, key: str, delta: int = ..., version: int | None = ...) -> int: ... + async def aclear(self) -> None: ... + def incr_version(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + async def aincr_version(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + def decr_version(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... + async def adecr_version(self, key: Any, delta: int = 1, version: int | None = None) -> int: ... def close(self, **kwargs: Any) -> None: ... + async def aclose(self, **kwargs: Any) -> None: ... + +memcached_error_chars_re: Pattern[str] + +def memcache_key_warnings(key: str) -> Iterator[str]: ... diff --git a/django-stubs/core/cache/backends/db.pyi b/django-stubs/core/cache/backends/db.pyi index 84dbbc12a..80c67aa15 100644 --- a/django-stubs/core/cache/backends/db.pyi +++ b/django-stubs/core/cache/backends/db.pyi @@ -1,23 +1,24 @@ -from typing import Any +from typing import Any, ClassVar from django.core.cache.backends.base import BaseCache from django.utils.functional import _StrOrPromise class Options: - db_table: str = ... - app_label: str = ... - model_name: str = ... - verbose_name: _StrOrPromise = ... - verbose_name_plural: _StrOrPromise = ... - object_name: str = ... - abstract: bool = ... - managed: bool = ... - proxy: bool = ... - swapped: bool = ... + db_table: str + app_label: str + model_name: str + verbose_name: _StrOrPromise + verbose_name_plural: _StrOrPromise + object_name: str + abstract: bool + managed: bool + proxy: bool + swapped: bool def __init__(self, table: str) -> None: ... class BaseDatabaseCache(BaseCache): - cache_model_class: Any = ... + cache_model_class: Any def __init__(self, table: str, params: dict[str, Any]) -> None: ... -class DatabaseCache(BaseDatabaseCache): ... +class DatabaseCache(BaseDatabaseCache): + pickle_protocol: ClassVar[int] diff --git a/django-stubs/core/cache/backends/filebased.pyi b/django-stubs/core/cache/backends/filebased.pyi index dd63c9650..956a47703 100644 --- a/django-stubs/core/cache/backends/filebased.pyi +++ b/django-stubs/core/cache/backends/filebased.pyi @@ -1,7 +1,8 @@ -from typing import Any +from typing import Any, ClassVar from django.core.cache.backends.base import BaseCache class FileBasedCache(BaseCache): - cache_suffix: str = ... + cache_suffix: ClassVar[str] + pickle_protocol: ClassVar[int] def __init__(self, dir: str, params: dict[str, Any]) -> None: ... diff --git a/django-stubs/core/cache/backends/locmem.pyi b/django-stubs/core/cache/backends/locmem.pyi index 3a158d074..c2634b004 100644 --- a/django-stubs/core/cache/backends/locmem.pyi +++ b/django-stubs/core/cache/backends/locmem.pyi @@ -1,6 +1,8 @@ -from typing import Any +from typing import Any, ClassVar from django.core.cache.backends.base import BaseCache class LocMemCache(BaseCache): + pickle_protocol: ClassVar[int] + def __init__(self, name: str, params: dict[str, Any]) -> None: ... diff --git a/django-stubs/core/cache/backends/memcached.pyi b/django-stubs/core/cache/backends/memcached.pyi index db93a70bd..08e648a17 100644 --- a/django-stubs/core/cache/backends/memcached.pyi +++ b/django-stubs/core/cache/backends/memcached.pyi @@ -1,12 +1,26 @@ +from collections.abc import Sequence +from types import ModuleType from typing import Any from django.core.cache.backends.base import BaseCache +from typing_extensions import override class BaseMemcachedCache(BaseCache): - def __init__(self, server: Any, params: Any, library: Any, value_not_found_exception: Any) -> None: ... - -class MemcachedCache(BaseMemcachedCache): - def __init__(self, server: Any, params: Any) -> None: ... + def __init__( + self, + server: str | Sequence[str], + params: dict[str, Any], + library: ModuleType, + value_not_found_exception: type[BaseException], + ) -> None: ... + @property + def client_servers(self) -> Sequence[str]: ... class PyLibMCCache(BaseMemcachedCache): - def __init__(self, server: Any, params: Any) -> None: ... + def __init__(self, server: str | Sequence[str], params: dict[str, Any]) -> None: ... + @property + @override + def client_servers(self) -> list[str]: ... + +class PyMemcacheCache(BaseMemcachedCache): + def __init__(self, server: str | Sequence[str], params: dict[str, Any]) -> None: ... diff --git a/django-stubs/core/cache/backends/redis.pyi b/django-stubs/core/cache/backends/redis.pyi new file mode 100644 index 000000000..e1a80909e --- /dev/null +++ b/django-stubs/core/cache/backends/redis.pyi @@ -0,0 +1,58 @@ +from collections.abc import Callable, Iterable, Mapping +from datetime import timedelta +from typing import Any, Protocol, SupportsInt, TypeAlias, overload, type_check_only + +from _typeshed import ReadableBuffer +from django.core.cache.backends.base import BaseCache +from django.utils.functional import cached_property +from redis._parsers import BaseParser +from redis.client import Redis +from redis.connection import ConnectionPool + +@type_check_only +class _RedisCacheClientSerializer(Protocol): + def dumps(self, obj: Any) -> bytes: ... + @overload + def loads(self, data: SupportsInt) -> int: ... + @overload + def loads(self, data: ReadableBuffer) -> Any: ... + +class RedisSerializer: + def __init__(self, protocol: int | None = None) -> None: ... + def dumps(self, obj: Any) -> bytes: ... + @overload + def loads(self, data: SupportsInt) -> int: ... + @overload + def loads(self, data: ReadableBuffer) -> Any: ... + +# Taken from https://github.com/redis/redis-py/blob/6b8978/redis/typing.py +_Key: TypeAlias = str | bytes | memoryview +_Expiry: TypeAlias = int | timedelta + +class RedisCacheClient: + def __init__( + self, + servers: list[str], + serializer: str | Callable[[], _RedisCacheClientSerializer] | _RedisCacheClientSerializer | None = None, + pool_class: str | type[ConnectionPool] | None = None, + parser_class: str | type[BaseParser] | None = None, + **options: Any, + ) -> None: ... + def get_client(self, key: _Key | None = None, *, write: bool = False) -> Redis: ... + def add(self, key: _Key, value: Any, timeout: _Expiry | None) -> bool: ... + def get(self, key: _Key, default: Any) -> Any: ... + def set(self, key: _Key, value: Any, timeout: _Expiry | None) -> None: ... + def touch(self, key: _Key, timeout: _Expiry) -> bool: ... + def delete(self, key: _Key) -> bool: ... + def get_many(self, keys: Iterable[_Key]) -> dict[_Key, Any]: ... + def has_key(self, key: _Key) -> bool: ... + def incr(self, key: _Key, delta: int) -> Any: ... + def set_many(self, data: Mapping[_Key, Any], timeout: _Expiry) -> None: ... + def delete_many(self, keys: Iterable[_Key]) -> None: ... + def clear(self) -> bool: ... + +class RedisCache(BaseCache): + def __init__(self, server: str | list[str], params: dict[str, Any]) -> None: ... + # We need this, because it is the only way to get the Redis connection from cache: + @cached_property + def _cache(self) -> RedisCacheClient: ... diff --git a/django-stubs/core/cache/utils.pyi b/django-stubs/core/cache/utils.pyi index 7a5e8b6ae..3be305227 100644 --- a/django-stubs/core/cache/utils.pyi +++ b/django-stubs/core/cache/utils.pyi @@ -3,4 +3,4 @@ from typing import Any TEMPLATE_FRAGMENT_KEY_TEMPLATE: str -def make_template_fragment_key(fragment_name: str, vary_on: Iterable[Any] | None = ...) -> str: ... +def make_template_fragment_key(fragment_name: str, vary_on: Iterable[Any] | None = None) -> str: ... diff --git a/django-stubs/utils/connection.pyi b/django-stubs/utils/connection.pyi index 56ddf64fb..1cca7ba7b 100644 --- a/django-stubs/utils/connection.pyi +++ b/django-stubs/utils/connection.pyi @@ -21,7 +21,7 @@ class ConnectionProxy(Generic[_T]): class ConnectionDoesNotExist(Exception): ... class BaseConnectionHandler(_SupportsContains[str], Generic[_T]): - settings_name: str | None + settings_name: str exception_class: type[Exception] thread_critical: bool @cached_property diff --git a/pyproject.toml b/pyproject.toml index 4b993b92a..a9d54e3fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dev = [ "pyright>=1.1.406", "ruff>=0.1.0", "tomlkit>=0.12.1", + "redis>=7.4.0", ] diff --git a/s/sync b/s/sync index 11301433a..45abf3e41 100755 --- a/s/sync +++ b/s/sync @@ -8,7 +8,7 @@ set -e DJANGO_STUBS_TARGET_TAG="6.0.5" git fetch https://github.com/typeddjango/django-stubs.git $DJANGO_STUBS_TARGET_TAG -git checkout $DJANGO_STUBS_TARGET_TAG -- django-stubs/tasks/ +git checkout $DJANGO_STUBS_TARGET_TAG -- django-stubs/tasks/ django-stubs/core/cache SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SYNC_FILES="$SCRIPT_DIR/sync-files.txt" diff --git a/uv.lock b/uv.lock index 438322db3..de1ca5759 100644 --- a/uv.lock +++ b/uv.lock @@ -60,6 +60,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/46/d3ec57ad500f598d1554bd14ce4df615960549ab2844961bc4e1f5fbd174/ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a", size = 1077165, upload-time = "2026-04-30T23:24:46.377Z" }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -94,6 +103,7 @@ dev = [ { name = "mypy" }, { name = "pyright" }, { name = "pytest" }, + { name = "redis" }, { name = "ruff" }, { name = "tomlkit" }, { name = "ty" }, @@ -110,6 +120,7 @@ dev = [ { name = "mypy", specifier = ">=2.1.0" }, { name = "pyright", specifier = ">=1.1.406" }, { name = "pytest", specifier = ">=7.1.1" }, + { name = "redis", specifier = ">=7.4.0" }, { name = "ruff", specifier = ">=0.1.0" }, { name = "tomlkit", specifier = ">=0.12.1" }, { name = "ty", specifier = ">=0.0.35" }, @@ -368,6 +379,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "redis" +version = "7.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, +] + [[package]] name = "ruff" version = "0.15.13"