Source code for eclypse.graph.invalidating_dict
"""Dictionary that calls a hook when selected keys change."""
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
)
if TYPE_CHECKING:
from collections.abc import (
Callable,
Iterable,
)
_MISSING = object()
[docs]
class InvalidatingDict(dict):
"""Dict that invalidates derived state when watched keys are mutated."""
[docs]
def __init__(
self,
*args: Any,
on_change: Callable[[tuple[Any, ...]], None],
watched_keys: Iterable[Any] | None = None,
**kwargs: Any,
):
"""Initialize the dictionary with an invalidation hook.
Args:
*args: Initial dictionary data.
on_change: Callback invoked with changed watched keys.
watched_keys: Keys that trigger invalidation. ``None`` watches every key.
**kwargs: Additional initial dictionary data.
"""
self._on_change = on_change
self._watched_keys = set(watched_keys) if watched_keys is not None else None
super().__init__(*args, **kwargs)
def _changed(self, keys: Iterable[Any]):
if not hasattr(self, "_on_change") or not hasattr(self, "_watched_keys"):
return
changed_keys = tuple(
k for k in keys if self._watched_keys is None or k in self._watched_keys
)
if changed_keys:
self._on_change(changed_keys)
def __setitem__(self, key: Any, value: Any):
"""Set a value and invalidate when it changes."""
old_value = self.get(key, _MISSING)
super().__setitem__(key, value)
if old_value != value:
self._changed((key,))
def __delitem__(self, key: Any):
"""Delete a key and invalidate."""
super().__delitem__(key)
self._changed((key,))
[docs]
def update(self, *args: Any, **kwargs: Any):
"""Update values and invalidate when any watched value changes."""
updates = dict(*args, **kwargs)
changed_keys = [
key for key, value in updates.items() if self.get(key, _MISSING) != value
]
super().update(updates)
if changed_keys:
self._changed(changed_keys)
[docs]
def setdefault(self, key: Any, default: Any = None):
"""Set a default value and invalidate only when a key is inserted."""
if key in self:
return self[key]
super().__setitem__(key, default)
self._changed((key,))
return default
[docs]
def pop(self, key: Any, default: Any = _MISSING):
"""Remove a key and invalidate when it existed."""
if key not in self:
if default is _MISSING:
raise KeyError(key)
return default
value = super().pop(key)
self._changed((key,))
return value
[docs]
def popitem(self):
"""Remove and return an item, then invalidate."""
key, value = super().popitem()
self._changed((key,))
return key, value
[docs]
def clear(self):
"""Remove all items and invalidate when the dictionary was non-empty."""
changed_keys = list(self)
super().clear()
if changed_keys:
self._changed(changed_keys)