| # Copilot Instructions for cachetools |
| |
| ## Architecture Overview |
| **cachetools** provides extensible memoizing collections and decorators, including variants of Python's `@lru_cache`. Pure Python 3.10+, no external runtime dependencies. |
| |
| ### Core Design Pattern |
| - All caches inherit from `Cache` (a `MutableMapping` with `maxsize`, `currsize`, and `getsizeof`) |
| - Subclasses override `__setitem__`, `__getitem__`, `__delitem__`, and `popitem()` to implement eviction policies |
| - **Critical:** Subclasses use default parameter trick (e.g., `cache_setitem=Cache.__setitem__`) to call parent methods efficiently while avoiding recursion |
| |
| ### Cache Types |
| - `FIFOCache`: Evicts oldest inserted (`OrderedDict`) |
| - `LRUCache`: Evicts least recently used (`OrderedDict.move_to_end()`) |
| - `LFUCache`: Evicts least frequently used (doubly-linked list of frequency buckets) |
| - `RRCache`: Random eviction (`__keys` list with `__index` dict for O(1) removal) |
| - `TTLCache`/`TLRUCache`: Time-based eviction via `_TimedCache` base; `_Timer` context manager freezes time during operations to prevent TOCTOU bugs; `expire()` returns `list[tuple[key, value]]` |
| |
| ### Decorators |
| - `@cached` (`_cached.py`): Function memoization; separate wrappers for each lock/condition/info combination; supports `cache=None` via `_uncached`/`_uncached_info` wrappers (pass-through without caching) |
| - `@cachedmethod` (`_cachedmethod.py`): Method memoization via descriptor protocol (`__set_name__`/`__get__`); class hierarchy: `_WrapperBase` (per-instance callable) → `_DescriptorBase` (descriptor with `__set_name__`/`__get__`, replaces self in instance `__dict__` via `setdefault` for thread safety) → `_DeprecatedDescriptorBase` (backward-compatible `@classmethod` support with warnings); the backward-compatible `_condition` variant uses `weakref.WeakKeyDictionary` for per-instance pending sets |
| - Both support `key`, `lock`, `condition`, and `info` parameters; when `condition` is given without `lock`, `condition` serves as both lock and condition |
| - `info=True` adds `cache_info()`/`cache_clear()`; `info=False` (default) only provides `cache_clear()` |
| - `func.py`: `functools.lru_cache`-compatible wrappers; all use `threading.Condition()` by default for thread safety + stampede prevention; `_UnboundTTLCache` extends `TTLCache` with `math.inf` maxsize for `maxsize=None` |
| |
| ### Thread Safety |
| 3-tier locking: **Unlocked** | **Locked** (release during compute) | **Condition** (lock + pending set + `wait_for`/`notify_all` to prevent thundering herd) |
| |
| `_AbstractCondition` protocol in `__init__.pyi`: extends `AbstractContextManager[Any]` + `Protocol` with `wait()`, `wait_for()`, `notify()`, `notify_all()`. Only `wait_for()` and `notify_all()` are used at runtime. |
| |
| ### Key Generation (`keys.py`) |
| - `hashkey`: Default key function; `_HashedTuple` caches hash values |
| - `methodkey`: Drops `self` from key; `typedkey`/`typedmethodkey`: Adds `type()` info |
| |
| ## Developer Workflows |
| |
| ### Testing |
| ```bash |
| pytest # Run all tests |
| pytest --cov=cachetools --cov-report term-missing # With coverage |
| tox -e py # Just tests |
| tox -e ruff # Linting (ruff check) |
| tox -e ruff-format # Format check (ruff format --diff) |
| tox -e pyright # Type checking |
| tox -e docs # Build docs |
| tox -e doctest # Run doctests |
| ``` |
| |
| - `tests/__init__.py`: `CacheTestMixin` (16 standard tests), `_TestCaseProtocol`, `CountedLock`, `CountedCondition` (implements full `_AbstractCondition` protocol) |
| - Each cache test inherits `unittest.TestCase` + `CacheTestMixin` |
| - `test_cached.py` / `test_cachedmethod.py` use `DecoratorTestMixin` / `MethodDecoratorTestMixin` for all lock/condition/info combos |
| - Threading stampede tests gated by `THREADING_TESTS` env var (enabled in `tox.ini` via `setenv`) |
| |
| ### Code Style |
| - **ruff** formatter and linter (`tox -e ruff-format`, `tox -e ruff`) |
| |
| ## Conventions |
| |
| ### Adding New Cache Types |
| 1. Inherit from `Cache` or `_TimedCache` |
| 2. Override `__setitem__`, `__delitem__`, `popitem()` (optionally `__getitem__`) |
| 3. Use default parameters to call parent: `def __setitem__(self, key, value, cache_setitem=Cache.__setitem__)` |
| 4. Handle `__missing__` edge case: check `if key in self` after parent call |
| 5. Add test class inheriting `CacheTestMixin` |
| |
| ### Type Stubs |
| Inline stubs ship with the package (`py.typed` marker): |
| - `@overload` distinguishes `info=True` vs `info=False`; `Literal[False]` overload listed last |
| - `_TimedCache` uses `Generic[_KT, _VT, _TT]` with `_TT` defaulting to `float` |
| - `_AbstractCondition` is `@type_check_only` `Protocol` for `condition` params and `cache_condition` attributes |
| - `_cached_wrapper` / `_cachedmethod_wrapper` use `ParamSpec(_P)` to preserve decorated function signatures; `__call__` uses `_P.args`/`_P.kwargs` |
| - `_cachedmethod_wrapper` models the descriptor protocol: `__set_name__`, `__get__`, `__call__`; uses `Concatenate[Any, _P]` so `_P` excludes `self` |
| - `_cachedmethod.py` uses `# type: ignore` for `functools.update_wrapper()` (typeshed #9846) |
| - Validate stubs with `tox -e pyright` |
| |
| ## Key Files |
| - `src/cachetools/__init__.py` — All cache implementations |
| - `src/cachetools/__init__.pyi` — Type stubs for caches and decorators |
| - `src/cachetools/_cached.py` — `@cached` decorator variants |
| - `src/cachetools/_cachedmethod.py` — `@cachedmethod` descriptor variants |
| - `src/cachetools/keys.py` / `keys.pyi` — Key functions |
| - `src/cachetools/func.py` / `func.pyi` — Functools-compatible wrappers (`lru_cache`, `ttl_cache`, etc.) |
| - `tests/__init__.py` — Test mixin and helpers |
| - `pyproject.toml` — Build config, version: `{attr = "cachetools.__version__"}` |