"""Sentinels, cycle error, and per-resolution cache used during DI resolution.
The `DependencyCache` object accumulates resolved dependency values
during a single resolution pass. The `_IN_PROGRESS` and `_CACHE_MISS`
sentinels separate the three cache-lookup outcomes (hit, miss, and
in-progress) without collapsing `None`-valued hits into misses.
"""
from __future__ import annotations
from typing import Any, Final
_IN_PROGRESS: object = object()
_CACHE_MISS: object = object()
REQUEST_DEP_CACHE_ATTR: Final[str] = "_next_dep_cache"
[docs]
def get_request_dep_cache(request: object | None) -> dict[str, Any] | None:
"""Return the dispatch-scoped dep cache attached to `request`, or `None`.
`FormActionDispatch.dispatch` attaches its `dep_cache` dict to the
request so downstream renderers (page context, component context)
can rejoin the same DI cache during a validation-failure re-render.
Consumers wrap the returned dict in `DependencyCache` to share state.
"""
if request is None:
return None
cache = getattr(request, REQUEST_DEP_CACHE_ATTR, None)
return cache if isinstance(cache, dict) else None
[docs]
class DependencyCycleError(Exception):
"""Raised when dependency resolution re-enters a key already in progress."""
[docs]
def __init__(self, cycle: list[str]) -> None:
"""Record the offending dependency chain for the error message."""
self.cycle = cycle
super().__init__(f"Circular dependency: {' -> '.join(cycle)}")
[docs]
class DependencyCache:
"""Store resolved dependency values and detect cycles via in-progress keys."""
[docs]
def __init__(self, backing_dict: dict[str, Any] | None = None) -> None:
"""Initialise storage, optionally sharing an externally owned dict."""
self._cache: dict[str, Any] = backing_dict if backing_dict is not None else {}
self._in_progress: set[str] = set()
self._owns_cache = backing_dict is None
[docs]
def get(self, key: str) -> object:
"""Return the cached value, `_IN_PROGRESS`, or `_CACHE_MISS`."""
if key in self._in_progress:
return _IN_PROGRESS
if key in self._cache:
return self._cache[key]
return _CACHE_MISS
[docs]
def set(self, key: str, value: object) -> None:
"""Store a finished resolution under the given key."""
self._cache[key] = value
self._in_progress.discard(key)
[docs]
def mark_in_progress(self, key: str) -> None:
"""Mark the key as currently being resolved for cycle detection."""
self._in_progress.add(key)
[docs]
def unmark_in_progress(self, key: str) -> None:
"""Clear the in-progress marker for the key."""
self._in_progress.discard(key)
[docs]
def is_in_progress(self, key: str) -> bool:
"""Return True while the key is mid-resolution."""
return key in self._in_progress
[docs]
def __len__(self) -> int:
"""Return the number of stored values."""
return len(self._cache)
[docs]
def __contains__(self, key: str) -> bool:
"""Return membership in the backing dict."""
return key in self._cache