masque/masque/utils/deferreddict.py

76 lines
2.5 KiB
Python

from typing import TypeVar, Generic
from collections.abc import Callable, Iterator
from functools import lru_cache
Key = TypeVar('Key')
Value = TypeVar('Value')
class DeferredDict(dict, Generic[Key, Value]):
"""
This is a modified `dict` which is used to defer loading/generating
values until they are accessed.
```
bignum = my_slow_function() # slow function call, would like to defer this
numbers = DeferredDict()
numbers['big'] = my_slow_function # no slow function call here
assert(bignum == numbers['big']) # first access is slow (function called)
assert(bignum == numbers['big']) # second access is fast (result is cached)
```
The `set_const` method is provided for convenience;
`numbers['a'] = lambda: 10` is equivalent to `numbers.set_const('a', 10)`.
"""
def __init__(self, *args, **kwargs) -> None:
dict.__init__(self)
if args or kwargs:
self.update(*args, **kwargs)
def __setitem__(self, key: Key, value: Callable[[], Value]) -> None:
"""
Set a value, which must be a callable that returns the actual value.
The result of the callable is cached after the first access.
"""
if not callable(value):
raise TypeError(f"DeferredDict value must be callable, got {type(value)}")
cached_fn = lru_cache(maxsize=1)(value)
dict.__setitem__(self, key, cached_fn)
def __getitem__(self, key: Key) -> Value:
return dict.__getitem__(self, key)()
def get(self, key: Key, default: Value | None = None) -> Value | None:
if key not in self:
return default
return self[key]
def items(self) -> Iterator[tuple[Key, Value]]:
for key in self.keys():
yield key, self[key]
def values(self) -> Iterator[Value]:
for key in self.keys():
yield self[key]
def update(self, *args, **kwargs) -> None:
"""
Update the DeferredDict. If a value is callable, it is used as a generator.
Otherwise, it is wrapped as a constant.
"""
for k, v in dict(*args, **kwargs).items():
if callable(v):
self[k] = v
else:
self.set_const(k, v)
def __repr__(self) -> str:
return '<DeferredDict with keys ' + repr(set(self.keys())) + '>'
def set_const(self, key: Key, value: Value) -> None:
"""
Convenience function to avoid having to manually wrap
constant values into callables.
"""
self[key] = lambda v=value: v