diff --git a/.pylintrc b/.pylintrc index 6054d77..328db8f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -57,7 +57,8 @@ ignore-paths= # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores # Emacs file locks -ignore-patterns=^\.# +ignore-patterns=^\.#, + .*\.pyi$ # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/CHANGELOG.md b/CHANGELOG.md index e85e43a..c284ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ diskcache-community forked from DiskCache at upstream commit ebfa37c (3 March 2024), two commits after the 5.6.3 release. Each entry names the pull request in this repository. Imported upstream pull requests are referenced by number, and their commits keep their original authors in the git history. +## Unreleased + +### Packaging + +- The package ships type information: `py.typed` and `.pyi` stubs covering the API, including `SafeDisk`, `SafeDisk.with_types`, `Disk.with_options`, `configure`, `default_disk`, `ReadOnlyError`, `read_only`, `store_time`, `iterkeys(tag=...)` and the `disk` arguments of `Deque`, `Index` and `FanoutCache.deque` and `index`, so mypy and pyright check code that uses the fork without a separate stubs package. `mypy.stubtest` reports no differences from the runtime (PR #29). +- `requires-python` is `>=3.9`, so the fork installs on Python 3.9; the test suite passes there unchanged (PR #29). + ## 5.8.0 (2026-10-18) The package is now imported as `diskcache_community`, so it installs next to the upstream `diskcache` package instead of replacing it. The `cache.db` schema, the file layout, the serialization modes, the memoize key format and the persisted settings are unchanged, so a 5.6.3 process opens a directory written by 5.8.0 and the reverse. `SafeDisk` stays opt-in and gains an allowlist for your own classes, a process-wide default disk selects it for caches that libraries create, and caches can treat unreadable entries as missing. diff --git a/diskcache_community/__init__.pyi b/diskcache_community/__init__.pyi new file mode 100644 index 0000000..3a52e0e --- /dev/null +++ b/diskcache_community/__init__.pyi @@ -0,0 +1,58 @@ +from diskcache_community.core import DEFAULT_SETTINGS as DEFAULT_SETTINGS +from diskcache_community.core import ENOVAL as ENOVAL +from diskcache_community.core import EVICTION_POLICY as EVICTION_POLICY +from diskcache_community.core import UNKNOWN as UNKNOWN +from diskcache_community.core import Cache as Cache +from diskcache_community.core import Disk as Disk +from diskcache_community.core import EmptyDirWarning as EmptyDirWarning +from diskcache_community.core import JSONDisk as JSONDisk +from diskcache_community.core import ReadOnlyError as ReadOnlyError +from diskcache_community.core import SafeDisk as SafeDisk +from diskcache_community.core import Timeout as Timeout +from diskcache_community.core import UnknownFileWarning as UnknownFileWarning +from diskcache_community.core import configure as configure +from diskcache_community.core import default_disk as default_disk +from diskcache_community.fanout import FanoutCache as FanoutCache +from diskcache_community.persistent import Deque as Deque +from diskcache_community.persistent import Index as Index +from diskcache_community.recipes import Averager as Averager +from diskcache_community.recipes import BoundedSemaphore as BoundedSemaphore +from diskcache_community.recipes import Lock as Lock +from diskcache_community.recipes import RLock as RLock +from diskcache_community.recipes import barrier as barrier +from diskcache_community.recipes import memoize_stampede as memoize_stampede +from diskcache_community.recipes import throttle as throttle + +__all__ = [ + 'Averager', + 'BoundedSemaphore', + 'Cache', + 'DEFAULT_SETTINGS', + 'Deque', + 'Disk', + 'ENOVAL', + 'EVICTION_POLICY', + 'EmptyDirWarning', + 'FanoutCache', + 'Index', + 'JSONDisk', + 'Lock', + 'RLock', + 'ReadOnlyError', + 'SafeDisk', + 'Timeout', + 'UNKNOWN', + 'UnknownFileWarning', + 'barrier', + 'configure', + 'default_disk', + 'memoize_stampede', + 'throttle', +] + +__title__: str +__version__: str +__build__: int +__author__: str +__license__: str +__copyright__: str diff --git a/diskcache_community/_types.pyi b/diskcache_community/_types.pyi new file mode 100644 index 0000000..e5a38d9 --- /dev/null +++ b/diskcache_community/_types.pyi @@ -0,0 +1,227 @@ +import warnings +from typing import ( + Any, + Callable, + Container, + ContextManager, + Iterator, + Literal, + Protocol, + overload, +) + +from _typeshed import Incomplete +from typing_extensions import ParamSpec, Self, TypeAlias, TypedDict, TypeVar + +from diskcache_community import Disk + +_T = TypeVar('_T') +_IntOrStrT = TypeVar('_IntOrStrT', bound=int | str) +_T_co = TypeVar('_T_co', covariant=True) +_P = ParamSpec('_P') + +DbName: TypeAlias = Literal['cache.db'] +ModeNone: TypeAlias = Literal[0] +ModeRaw: TypeAlias = Literal[1] +ModeBinary: TypeAlias = Literal[2] +ModeText: TypeAlias = Literal[3] +ModePickle: TypeAlias = Literal[4] +ModeLiteral: TypeAlias = ( + ModeNone | ModeRaw | ModeBinary | ModeText | ModePickle +) +EnovalType: TypeAlias = Literal['ENOVAL'] +UnknownType: TypeAlias = Literal['UNKNOWN'] + +ExpireTime: TypeAlias = float | None +Tag: TypeAlias = str | None +Ignore: TypeAlias = Container[int | str] + +ServerSide: TypeAlias = Literal['back', 'front'] +KeyValuePair: TypeAlias = tuple[_IntOrStrT, Any] +NullPair: TypeAlias = tuple[None, None] +NullablePair: TypeAlias = KeyValuePair[_IntOrStrT] | NullPair + +EvictionPolicyKey: TypeAlias = Literal[ + 'none', + 'least-recently-stored', + 'least-recently-used', + 'least-frequently-used', +] +EvictionPolicyItemKey: TypeAlias = Literal['init', 'get', 'cull'] +EvictionPolicyItem: TypeAlias = dict[EvictionPolicyItemKey, str | None] + +class Memoized(Protocol[_P, _T_co]): + def __cache_key__( + self, *args: _P.args, **kwargs: _P.kwargs + ) -> tuple[Any, ...]: ... + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T_co: ... + +class Settings(TypedDict, total=False): + statistics: int + tag_index: int + eviction_policy: str + size_limit: int + cull_limit: int + sqlite_auto_vacuum: int + sqlite_cache_size: int + sqlite_journal_mode: str + sqlite_mmap_size: int + sqlite_synchronous: int + disk_min_file_size: int + disk_pickle_protocol: int + +class DefaultSettings(TypedDict, total=True): + statistics: int + tag_index: int + eviction_policy: str + size_limit: int + cull_limit: int + sqlite_auto_vacuum: int + sqlite_cache_size: int + sqlite_journal_mode: str + sqlite_mmap_size: int + sqlite_synchronous: int + disk_min_file_size: int + disk_pickle_protocol: int + +class Metadata(TypedDict, total=False): + count: int + size: int + hits: int + misses: int + +class DefaultMetadata(TypedDict, total=True): + count: int + size: int + hits: int + misses: int + +class InitSettings(TypedDict, total=False): + # settings + statistics: int + tag_index: int + eviction_policy: str + size_limit: int + cull_limit: int + sqlite_auto_vacuum: int + sqlite_cache_size: int + sqlite_journal_mode: str + sqlite_mmap_size: int + sqlite_synchronous: int + disk_min_file_size: int + disk_pickle_protocol: int + # metadata + count: int + size: int + hits: int + misses: int + +class BaseCache(Protocol): + ### Settings + @property + def statistics(self) -> int: ... + @property + def tag_index(self) -> int: ... + @property + def eviction_policy(self) -> str: ... + @property + def size_limit(self) -> int: ... + @property + def cull_limit(self) -> int: ... + @property + def sqlite_auto_vacuum(self) -> int: ... + @property + def sqlite_cache_size(self) -> int: ... + @property + def sqlite_journal_mode(self) -> str: ... + @property + def sqlite_mmap_size(self) -> int: ... + @property + def sqlite_synchronous(self) -> int: ... + @property + def disk_min_file_size(self) -> int: ... + @property + def disk_pickle_protocol(self) -> int: ... + ### + @property + def directory(self) -> str: ... + @property + def timeout(self) -> int: ... + @property + def disk(self) -> Disk: ... + def transact(self, retry: bool = ...) -> ContextManager[None]: ... + def set( # noqa: PLR0913 + self, + key: Any, + value: Any, + expire: ExpireTime = ..., + read: bool = ..., + tag: Tag = ..., + retry: bool = ..., + ) -> Literal[True]: ... + def __setitem__(self, key: Any, value: Any) -> None: ... + def touch( + self, key: Any, expire: ExpireTime = ..., retry: bool = ... + ) -> bool: ... + def add( # noqa: PLR0913 + self, + key: Any, + value: Any, + expire: ExpireTime = ..., + read: bool = ..., + tag: Tag = ..., + retry: bool = ..., + ) -> bool: ... + def incr( + self, key: Any, delta: int = ..., default: int = ..., retry: bool = ... + ) -> int: ... + def decr( + self, key: Any, delta: int = ..., default: int = ..., retry: bool = ... + ) -> int: ... + def get( # noqa: PLR0913 + self, + key: Any, + default: Any = ..., + read: bool = ..., + expire_time: bool = ..., + tag: bool = ..., + retry: bool = ..., + store_time: bool = ..., + ) -> Incomplete: ... + def __getitem__(self, key: Any) -> Any: ... + def __contains__(self, key: Any) -> bool: ... + def pop( + self, + key: Any, + default: Any = ..., + expire_time: bool = ..., + tag: bool = ..., + retry: bool = ..., + ) -> Incomplete: ... + def delete(self, key: Any, retry: bool = ...) -> bool: ... + def create_tag_index(self) -> None: ... + def drop_tag_index(self) -> None: ... + def check( + self, fix: bool = ..., retry: bool = ... + ) -> list[warnings.WarningMessage]: ... + def evict(self, tag: str, retry: bool = ...) -> int: ... + def cull(self, retry: bool = ...) -> int: ... + def clear(self, retry: bool = ...) -> int: ... + def __iter__(self) -> Iterator[Any]: ... + def __reversed__(self) -> Iterator[Any]: ... + def stats( + self, enable: bool = ..., reset: bool = ... + ) -> tuple[Any, Any]: ... + def volume(self) -> int: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, *exception: object) -> None: ... + def __len__(self) -> int: ... + def memoize( + self, + name: str | None = ..., + typed: bool = ..., + expire: ExpireTime = ..., + tag: Tag = ..., + ignore: Ignore = ..., + ) -> Callable[[Callable[_P, _T]], Memoized[_P, _T]]: ... diff --git a/diskcache_community/core.pyi b/diskcache_community/core.pyi new file mode 100644 index 0000000..0af9fb2 --- /dev/null +++ b/diskcache_community/core.pyi @@ -0,0 +1,327 @@ +import pickle +import sqlite3 +from typing import ( + IO, + Any, + BinaryIO, + Callable, + Final, + Generator, + Generic, + Literal, + overload, +) + +from _typeshed import StrOrBytesPath +from typing_extensions import Self, TypeVar, Unpack + +from ._types import ( + BaseCache, + DbName, + DefaultMetadata, + DefaultSettings, + EnovalType, + EvictionPolicyItem, + EvictionPolicyKey, + ExpireTime, + Ignore, + InitSettings, + KeyValuePair, + ModeBinary, + ModeLiteral, + ModeNone, + ModePickle, + ModeRaw, + ModeText, + NullablePair, + ServerSide, + Tag, + UnknownType, +) + +_T = TypeVar('_T') +_BoundStrT = TypeVar('_BoundStrT', bound=str) +_PutT1 = TypeVar('_PutT1', bound=str | int | float) +_PutT2 = TypeVar('_PutT2', bound=int | float) + +def full_name(func: Callable[..., Any]) -> str: ... +def native_filename(filename: str) -> str: ... + +class Constant(tuple[_BoundStrT], Generic[_BoundStrT]): # noqa: SLOT001 + def __new__(cls, name: _BoundStrT) -> Self: ... + +DBNAME: Final[DbName] +ENOVAL: Final[Constant[EnovalType]] +UNKNOWN: Final[Constant[UnknownType]] +MODE_NONE: Final[ModeNone] +MODE_RAW: Final[ModeRaw] +MODE_BINARY: Final[ModeBinary] +MODE_TEXT: Final[ModeText] +MODE_PICKLE: Final[ModePickle] +DEFAULT_SETTINGS: Final[DefaultSettings] +METADATA: Final[DefaultMetadata] +EVICTION_POLICY: Final[dict[EvictionPolicyKey, EvictionPolicyItem]] +SETTINGS_TRIGGERS: Final[tuple[tuple[str, str], ...]] +SAFE_TYPES: Final[frozenset[type]] +SAFE_PICKLE_NAMES: Final[dict[str, frozenset[str]]] +UNSAFE_MODULES: Final[tuple[str, ...]] +ENV_DISK: Final[str] +ENV_SAFE_TYPES: Final[str] +ENV_SAFE_MODULES: Final[str] + +class Disk: + min_file_size: int + pickle_protocol: int + on_error: Literal['raise', 'miss'] + @classmethod + def with_options( + cls, *, on_error: Literal['raise', 'miss'] + ) -> type[Self]: ... + def __init__( + self, + directory: StrOrBytesPath, + min_file_size: int = ..., + pickle_protocol: int = ..., + ) -> None: ... + def hash(self, key: Any) -> int: ... + @overload + def put(self, key: bytes) -> tuple[sqlite3.Binary, Literal[True]]: ... + @overload + def put(self, key: _PutT1) -> tuple[_PutT1, Literal[True]]: ... + @overload + def put(self, key: Any) -> tuple[sqlite3.Binary, Literal[False]]: ... + @overload + def get(self, key: sqlite3.Binary, raw: Literal[True]) -> bytes: ... + @overload + def get(self, key: _T, raw: Literal[True]) -> bytes | _T: ... + @overload + def get(self, key: Any, raw: Literal[False]) -> Any: ... + @overload + def get(self, key: Any, raw: bool) -> Any: ... + @overload + def store( + self, value: _PutT2, read: bool, key: Any = ... + ) -> tuple[Literal[0], ModeRaw, None, _PutT2]: ... + @overload + def store( + self, value: bytes, read: bool, key: Any = ... + ) -> ( + tuple[Literal[0], ModeRaw, None, sqlite3.Binary] + | tuple[int, ModeBinary, str, None] + ): ... + @overload + def store( + self, value: str, read: bool, key: Any = ... + ) -> tuple[Literal[0], ModeRaw, None, str] | tuple[ + int, ModeText, str, None + ]: ... + @overload + def store( + self, value: Any, read: Literal[True], key: Any = ... + ) -> tuple[int, ModeBinary, str, None]: ... + @overload + def store( + self, value: Any, read: bool, key: Any = ... + ) -> ( + tuple[Literal[0], ModePickle, None, sqlite3.Binary] + | tuple[int, ModePickle, str, None] + ): ... + @overload + def fetch( + self, mode: ModeRaw, filename: str, value: sqlite3.Binary, read: bool + ) -> bytes: ... + @overload + def fetch( + self, mode: ModeRaw, filename: str, value: _T, read: bool + ) -> _T: ... + @overload + def fetch( + self, mode: ModeBinary, filename: str, value: Any, read: Literal[True] + ) -> BinaryIO: ... + @overload + def fetch( + self, mode: ModeBinary, filename: str, value: Any, read: Literal[False] + ) -> BinaryIO: ... + @overload + def fetch( + self, mode: ModeBinary, filename: str, value: Any, read: bool + ) -> BinaryIO | bytes: ... + @overload + def fetch( + self, mode: ModeText, filename: str, value: Any, read: bool + ) -> str: ... + @overload + def fetch( + self, mode: ModePickle, filename: str, value: Any, read: bool + ) -> Any: ... + @overload + def fetch( + self, mode: ModeLiteral, filename: str, value: Any, read: bool + ) -> Any: ... + def filename( + self, key: Any = ..., value: Any = ... + ) -> tuple[str, str]: ... + def remove(self, file_path: StrOrBytesPath) -> None: ... + +class JSONDisk(Disk): + compress_level: int + def __init__( + self, + directory: StrOrBytesPath, + compress_level: int = ..., + *, + # Disk init args + min_file_size: int = ..., + pickle_protocol: int = ..., + ) -> None: ... + +class SafeUnpickler(pickle.Unpickler): + def __init__( + self, + file: IO[bytes], + names: dict[str, frozenset[str]] | None = ..., + classes: tuple[type, ...] = ..., + modules: tuple[str, ...] = ..., + ) -> None: ... + def find_class(self, module: str, name: str) -> Any: ... + +class SafeDisk(Disk): + safe_types: frozenset[type] + safe_modules: tuple[str, ...] + @classmethod + def with_types( + cls, *types: type, modules: tuple[str, ...] = ... + ) -> type[Self]: ... + +def configure(disk: type[Disk] | None) -> None: ... +def default_disk() -> type[Disk]: ... + +class Timeout(Exception): ... # noqa: N818 +class ReadOnlyError(sqlite3.OperationalError): ... +class UnknownFileWarning(UserWarning): ... +class EmptyDirWarning(UserWarning): ... + +def args_to_key( + base: tuple[Any, ...], + args: tuple[Any, ...], + kwargs: dict[str, Any], + typed: bool, + ignore: Ignore, +) -> tuple[Any, ...]: ... +def resolve_ignore(func: Callable[..., Any], ignore: Ignore) -> Ignore: ... + +class Cache(BaseCache): + @overload + def __init__( + self, + directory: StrOrBytesPath | None = ..., + timeout: int = ..., + disk: type[Disk] | None = ..., + *, + read_only: bool = ..., + **settings: Unpack[InitSettings], + ) -> None: ... + @overload + def __init__( + self, + directory: StrOrBytesPath | None = ..., + timeout: int = ..., + disk: type[Disk] | None = ..., + *, + read_only: bool = ..., + # FIXME + # https://peps.python.org/pep-0728/ + # diskcache allow "disk_*" args + **settings: Any, + ) -> None: ... + @property + def read_only(self) -> bool: ... + @property + def _con(self) -> sqlite3.Connection: ... + @property + def _sql(self) -> Callable[..., sqlite3.Cursor]: ... + def __delitem__(self, key: Any, retry: bool = ...) -> Literal[True]: ... + @overload + def reset(self, key: str, value: _T, update: bool = ...) -> _T: ... + @overload + def reset(self, key: str, value: Any = ..., update: bool = ...) -> Any: ... + def read(self, key: Any, retry: bool = ...) -> Any: ... + @overload + def push( + self, + value: Any, + prefix: None = ..., + side: ServerSide = ..., + expire: ExpireTime = ..., + read: bool = ..., + tag: Tag = ..., + retry: bool = ..., + ) -> int: ... + @overload + def push( + self, + value: Any, + prefix: str = ..., + side: ServerSide = ..., + expire: ExpireTime = ..., + read: bool = ..., + tag: Tag = ..., + retry: bool = ..., + ) -> str: ... + @overload + def push( + self, + value: Any, + prefix: str | None = ..., + side: ServerSide = ..., + expire: ExpireTime = ..., + read: bool = ..., + tag: Tag = ..., + retry: bool = ..., + ) -> str | int: ... + def pull( # noqa: PLR0913 + self, + prefix: str | None = ..., + default: Any = ..., + side: ServerSide = ..., + expire_time: bool = ..., + tag: bool = ..., + retry: bool = ..., + ) -> ( + NullablePair[Any] + | tuple[NullablePair[Any], Tag] + | tuple[NullablePair[Any], ExpireTime] + | tuple[NullablePair[Any], ExpireTime, Tag] + ): ... + def peek( # noqa: PLR0913 + self, + prefix: str | None = ..., + default: Any = ..., + side: ServerSide = ..., + expire_time: bool = ..., + tag: bool = ..., + retry: bool = ..., + ) -> ( + NullablePair[Any] + | tuple[NullablePair[Any], Tag] + | tuple[NullablePair[Any], ExpireTime] + | tuple[NullablePair[Any], ExpireTime, Tag] + ): ... + def peekitem( + self, + last: bool = ..., + expire_time: bool = ..., + tag: bool = ..., + retry: bool = ..., + ) -> ( + KeyValuePair[Any] + | tuple[KeyValuePair[Any], Tag] + | tuple[KeyValuePair[Any], ExpireTime] + | tuple[KeyValuePair[Any], ExpireTime, Tag] + ): ... + def expire(self, now: ExpireTime = ..., retry: bool = ...) -> int: ... + def iterkeys( + self, reverse: bool = ..., tag: Tag | Constant[EnovalType] = ... + ) -> Generator[Any, None, None]: ... + def __getstate__(self) -> tuple[str, int, type[Disk]]: ... + def __setstate__(self, state: tuple[str, int, type[Disk]]) -> None: ... diff --git a/diskcache_community/djangocache.pyi b/diskcache_community/djangocache.pyi new file mode 100644 index 0000000..cfdd37c --- /dev/null +++ b/diskcache_community/djangocache.pyi @@ -0,0 +1,101 @@ +# pyright: basic +# ruff: noqa +# mypy: ignore-errors +from _typeshed import Incomplete +from django.core.cache.backends.base import ( # pyright: ignore[reportMissingImports] + BaseCache, +) + +class DjangoCache(BaseCache): + def __init__(self, directory, params) -> None: ... + @property + def directory(self): ... + def cache(self, name): ... + def deque(self, name, maxlen: Incomplete | None = None): ... + def index(self, name): ... + def add( + self, + key, + value, + timeout=..., + version: Incomplete | None = None, + read: bool = False, + tag: Incomplete | None = None, + retry: bool = True, + ): ... + def get( + self, + key, + default: Incomplete | None = None, + version: Incomplete | None = None, + read: bool = False, + expire_time: bool = False, + tag: bool = False, + retry: bool = False, + store_time: bool = False, + ): ... + def read(self, key, version: Incomplete | None = None): ... + def set( + self, + key, + value, + timeout=..., + version: Incomplete | None = None, + read: bool = False, + tag: Incomplete | None = None, + retry: bool = True, + ): ... + def touch( + self, + key, + timeout=..., + version: Incomplete | None = None, + retry: bool = True, + ): ... + def pop( + self, + key, + default: Incomplete | None = None, + version: Incomplete | None = None, + expire_time: bool = False, + tag: bool = False, + retry: bool = True, + ): ... + def delete( + self, key, version: Incomplete | None = None, retry: bool = True + ): ... + def incr( + self, + key, + delta: int = 1, + version: Incomplete | None = None, + default: Incomplete | None = None, + retry: bool = True, + ): ... + def decr( + self, + key, + delta: int = 1, + version: Incomplete | None = None, + default: Incomplete | None = None, + retry: bool = True, + ): ... + def has_key(self, key, version: Incomplete | None = None): ... + def expire(self): ... + def stats(self, enable: bool = True, reset: bool = False): ... + def create_tag_index(self) -> None: ... + def drop_tag_index(self) -> None: ... + def evict(self, tag): ... + def cull(self): ... + def clear(self): ... + def close(self, **kwargs) -> None: ... + def get_backend_timeout(self, timeout=...): ... + def memoize( + self, + name: Incomplete | None = None, + timeout=..., + version: Incomplete | None = None, + typed: bool = False, + tag: Incomplete | None = None, + ignore=(), + ): ... diff --git a/diskcache_community/fanout.pyi b/diskcache_community/fanout.pyi new file mode 100644 index 0000000..d8650a7 --- /dev/null +++ b/diskcache_community/fanout.pyi @@ -0,0 +1,70 @@ +from typing import Any, overload + +from _typeshed import StrOrBytesPath +from typing_extensions import Unpack + +from diskcache_community.core import DEFAULT_SETTINGS as DEFAULT_SETTINGS +from diskcache_community.core import ENOVAL as ENOVAL +from diskcache_community.core import Cache as Cache +from diskcache_community.core import Disk as Disk +from diskcache_community.core import Timeout as Timeout +from diskcache_community.persistent import Deque as Deque +from diskcache_community.persistent import Index as Index + +from ._types import BaseCache, InitSettings + +class FanoutCache(BaseCache): + @overload + def __init__( + self, + directory: StrOrBytesPath | None = ..., + shards: int = ..., + timeout: float = ..., + disk: type[Disk] | None = ..., + **settings: Unpack[InitSettings], + ) -> None: ... + @overload + def __init__( + self, + directory: StrOrBytesPath | None = ..., + shards: int = ..., + timeout: float = ..., + disk: type[Disk] | None = ..., + # FIXME + # https://peps.python.org/pep-0728/ + # diskcache allow "disk_*" args + **settings: Any, + ) -> None: ... + def __getattr__(self, name: str) -> Any: ... + def read(self, key: Any) -> Any: ... + def expire(self, retry: bool = ...) -> int: ... + @overload + def cache( + self, + name: str, + timeout: int = ..., + disk: type[Disk] | None = ..., + **settings: Unpack[InitSettings], + ) -> Cache: ... + @overload + def cache( + self, + name: str, + timeout: int = ..., + disk: type[Disk] | None = ..., + # FIXME + # https://peps.python.org/pep-0728/ + # diskcache allow "disk_*" args + **settings: Any, + ) -> Cache: ... + def deque( + self, + name: str, + maxlen: int | None = ..., + disk: type[Disk] | None = ..., + ) -> Deque[Any]: ... + def index( + self, name: str, disk: type[Disk] | None = ... + ) -> Index[Any, Any]: ... + def __delitem__(self, key: Any) -> None: ... + def reset(self, key: str, value: Any = ...) -> Any: ... diff --git a/diskcache_community/persistent.pyi b/diskcache_community/persistent.pyi new file mode 100644 index 0000000..51b4b6d --- /dev/null +++ b/diskcache_community/persistent.pyi @@ -0,0 +1,161 @@ +from collections.abc import MutableMapping, Sequence +from typing import ( + Any, + Callable, + ContextManager, + Generic, + ItemsView, + Iterable, + Iterator, + KeysView, + ValuesView, + overload, +) + +from _typeshed import StrOrBytesPath +from typing_extensions import ParamSpec, Self, TypeVar + +from diskcache_community.core import Cache, Disk + +from ._types import Ignore, KeyValuePair, Memoized, NullablePair, ServerSide + +_AnyDefault: Iterable[Any] = () +_AnyT = TypeVar('_AnyT', bound=Any) +_KeyT = TypeVar('_KeyT', bound=Any, default=Any) +_ValueT = TypeVar('_ValueT', bound=Any, default=Any) +_T = TypeVar('_T') +_P = ParamSpec('_P') + +class Deque(Sequence[_ValueT], Generic[_ValueT]): + __hash__: None # type: ignore[assignment] + def __init__( + self, + iterable: Iterable[_ValueT] = _AnyDefault, # noqa: PYI011 + directory: StrOrBytesPath | None = ..., + maxlen: int | None = ..., + disk: type[Disk] | None = ..., + ) -> None: ... + @overload + @classmethod + def fromcache( + cls, cache: Cache, iterable: Iterable[_AnyT], maxlen: int | None = ... + ) -> Deque[_AnyT]: ... + @overload + @classmethod + def fromcache( + cls, + cache: Cache, + iterable: Iterable[Any] = ..., + maxlen: int | None = ..., + ) -> Deque[Any]: ... + @property + def cache(self) -> Cache: ... + @property + def directory(self) -> str: ... + @property + def maxlen(self) -> int: ... + @maxlen.setter + def maxlen(self, value: int) -> None: ... + def __getitem__(self, index: int) -> Any: ... # type: ignore[override] + def __setitem__(self, index: int, value: Any) -> None: ... + def __delitem__(self, index: int) -> None: ... + def __eq__(self, value: object) -> bool: ... + def __ne__(self, value: object) -> bool: ... + def __lt__(self, value: object) -> bool: ... + def __gt__(self, value: object) -> bool: ... + def __le__(self, value: object) -> bool: ... + def __ge__(self, value: object) -> bool: ... + def __iadd__(self, iterable: Iterable[_ValueT]) -> Self: ... + def __iter__(self) -> Iterator[Any]: ... + def __len__(self) -> int: ... + def __reversed__(self) -> Iterator[Any]: ... + def __getstate__(self) -> tuple[str, int]: ... + def __setstate__(self, state: tuple[str, int]) -> None: ... + def append(self, value: _ValueT) -> None: ... + def appendleft(self, value: _ValueT) -> None: ... + def clear(self) -> None: ... + def copy(self) -> Self: ... + def count(self, value: _ValueT) -> int: ... + def extend(self, iterable: Iterable[_ValueT]) -> None: ... + def extendleft(self, iterable: Iterable[_ValueT]) -> None: ... + def peek(self) -> _ValueT: ... + def peekleft(self) -> _ValueT: ... + def pop(self) -> _ValueT: ... + def popleft(self) -> _ValueT: ... + def remove(self, value: _ValueT) -> _ValueT: ... + def reverse(self) -> None: ... + def rotate(self, steps: int = ...) -> None: ... + def transact(self) -> ContextManager[None]: ... + +class Index(MutableMapping[_KeyT, _ValueT], Generic[_KeyT, _ValueT]): + __hash__: None # type: ignore[assignment] + def __init__( + self, *args: Any, disk: type[Disk] | None = ..., **kwargs: Any + ) -> None: ... + @classmethod + def fromcache( + cls, cache: Cache, *args: Any, **kwargs: Any + ) -> Index[Any, Any]: ... + @property + def cache(self) -> Cache: ... + @property + def directory(self) -> str: ... + def __getitem__(self, key: _KeyT) -> _ValueT: ... + def __setitem__(self, key: _KeyT, value: _ValueT) -> None: ... + def __delitem__(self, key: _KeyT) -> None: ... + @overload + def setdefault( + self, key: _KeyT, default: _ValueT | None = ... + ) -> _ValueT: ... + @overload + def setdefault(self, key: _KeyT, default: Any = ...) -> Any: ... + def peekitem(self, last: bool = ...) -> KeyValuePair[Any]: ... + @overload + def pop(self, key: _KeyT, default: _ValueT = ...) -> _ValueT: ... + @overload + def pop( + self, key: _KeyT, default: Any = ... + ) -> Any: ... # pyright: ignore[reportIncompatibleMethodOverride] + def popitem(self, last: bool = True) -> KeyValuePair[Any]: ... + @overload + def push( + self, value: _ValueT, prefix: None = ..., side: ServerSide = ... + ) -> int: ... + @overload + def push( + self, value: _ValueT, prefix: str = ..., side: ServerSide = ... + ) -> str: ... + @overload + def push( + self, value: _ValueT, prefix: str | None = ..., side: ServerSide = ... + ) -> str | int: ... + @overload + def pull( + self, prefix: None = ..., default: Any = ..., side: ServerSide = ... + ) -> NullablePair[int]: ... + @overload + def pull( + self, prefix: str = ..., default: Any = ..., side: ServerSide = ... + ) -> NullablePair[str]: ... + @overload + def pull( + self, + prefix: str | None = ..., + default: Any = ..., + side: ServerSide = ..., + ) -> NullablePair[Any]: ... + def clear(self) -> None: ... + def __iter__(self) -> Iterator[Any]: ... + def __reversed__(self) -> Iterator[Any]: ... + def __len__(self) -> int: ... + def keys(self) -> KeysView[_KeyT]: ... + def values(self) -> ValuesView[_ValueT]: ... + def items(self) -> ItemsView[_KeyT, _ValueT]: ... + def __getstate__(self) -> str: ... + def __setstate__(self, state: str) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def memoize( + self, name: str | None = ..., typed: bool = ..., ignore: Ignore = ... + ) -> Callable[[Callable[_P, _T]], Memoized[_P, _T]]: ... + def transact(self) -> ContextManager[None]: ... diff --git a/diskcache_community/py.typed b/diskcache_community/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/diskcache_community/recipes.pyi b/diskcache_community/recipes.pyi new file mode 100644 index 0000000..d9c4f5c --- /dev/null +++ b/diskcache_community/recipes.pyi @@ -0,0 +1,102 @@ +from typing import Any, Callable, Protocol + +from typing_extensions import ParamSpec, TypeVar, override + +from ._types import BaseCache, ExpireTime, Ignore, Tag + +_T = TypeVar('_T') +_P = ParamSpec('_P') + +class Averager: + def __init__( + self, + cache: BaseCache, + key: Any, + expire: ExpireTime = ..., + tag: Tag = ..., + ) -> None: ... + def add(self, value: float) -> None: ... + def get(self) -> float | None: ... + def pop(self) -> float | None: ... + +class _Lock(Protocol): + def acquire(self) -> Any: ... + def release(self) -> Any: ... + def __enter__(self) -> Any: ... + def __exit__(self, *exc_info: object) -> Any: ... + +class Lock(_Lock): + def __init__( + self, + cache: BaseCache, + key: Any, + expire: ExpireTime = ..., + tag: Tag = ..., + ) -> None: ... + def locked(self) -> bool: ... + @override + def acquire(self) -> None: ... + @override + def release(self) -> None: ... + @override + def __enter__(self) -> None: ... + @override + def __exit__(self, *exc_info: object) -> None: ... + +class RLock(_Lock): + def __init__( + self, + cache: BaseCache, + key: Any, + expire: ExpireTime = ..., + tag: Tag = ..., + ) -> None: ... + @override + def acquire(self) -> None: ... + @override + def release(self) -> None: ... + @override + def __enter__(self) -> None: ... + @override + def __exit__(self, *exc_info: object) -> None: ... + +class BoundedSemaphore: + def __init__( + self, + cache: BaseCache, + key: Any, + value: int = ..., + expire: ExpireTime = ..., + tag: Tag = ..., + ) -> None: ... + def acquire(self) -> None: ... + def release(self) -> None: ... + def __enter__(self) -> None: ... + def __exit__(self, *exc_info: object) -> None: ... + +def throttle( # noqa: PLR0913 + cache: BaseCache, + count: int, + seconds: float, + name: str | None = ..., + expire: ExpireTime = ..., + tag: Tag = ..., + time_func: Callable[[], float] = ..., + sleep_func: Callable[[float], Any] = ..., +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ... +def barrier( + cache: BaseCache, + lock_factory: Callable[[BaseCache, Any, ExpireTime, Tag], _Lock], + name: str | None = ..., + expire: ExpireTime = ..., + tag: Tag = ..., +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ... +def memoize_stampede( # noqa: PLR0913 + cache: BaseCache, + expire: float, + name: str | None = ..., + typed: bool = ..., + tag: Tag = ..., + beta: float = ..., + ignore: Ignore = ..., +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: ... diff --git a/pyproject.toml b/pyproject.toml index 4d1e1d3..ee1f87f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "diskcache-community" dynamic = ["version"] description = "Disk Cache -- Disk and file backed persistent cache." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.9" license = "Apache-2.0" license-files = ["LICENSE", "NOTICE"] authors = [{ name = "Grant Jenks" }]