Source code for duck.utils.lazy

"""
Lazily evaluates a callable and caches its result with an LRU policy.
"""
import types
import asyncio
import functools

from collections import OrderedDict
from typing import Callable, Tuple, Any


[docs] class LazyError(Exception): """ Custom exception for Lazy evaluation errors. """ pass
[docs] class Lazy: """ Lazily evaluates a callable and caches its result with an LRU policy. Supports automatic delegation of most magic methods, including: - Standard dunders (len, iter, str, etc.) - Awaitable (async/await) - Context manager - Async generator Notes: - You can set `nocache=True` to compute live results. """ _global_cache: OrderedDict[Tuple[Callable, Tuple, Tuple], Any] = OrderedDict() _max_size: int = 256 __slots__ = ( "_Lazy__callable", "_Lazy__args", "_Lazy__kwargs", "_Lazy__result", "_Lazy__nocache", "_Lazy__extra_data", ) __all_private_attrs = {} def __init__(self, _callable: Callable, nocache: bool = False, *args, **kwargs): self.__callable = _callable self.__args = args self.__kwargs = kwargs self.__nocache = nocache self.__extra_data = {} if not type(self).__all_private_attrs: type(self).__all_private_attrs = { "__getattr__", "__setattr__", "getresult", "__class__", "__call__", "__bool__", "extra_data", } @property def extra_data(self) -> dict: """ Returns the dictionary for storing extra data. """ return self.__extra_data
[docs] def __getattribute__(self, key): if key in type(self).__all_private_attrs or key in type(self).__slots__: return super().__getattribute__(key) try: return getattr(super().__getattribute__('getresult')(), key) except AttributeError as e: raise LazyError(f"Attribute '{key}' not found in lazy-loaded object: {e}") from e except Exception as e: raise LazyError(f"Error accessing attribute '{key}': {e}") from e
[docs] def __setattr__(self, key, value): if key in (*type(self).__slots__, *type(self).__all_private_attrs): super().__setattr__(key, value) else: try: setattr(self.getresult(), key, value) except Exception as e: raise LazyError(f"Error setting attribute '{key}': {e}")
[docs] def __bool__(self): return bool(self.getresult())
[docs] def __call__(self, *args, **kwargs): result = self.getresult() try: return result(*args, **kwargs) if (args or kwargs) else result except Exception as e: raise LazyError(f"Error calling lazy object: {e}")
[docs] def getresult(self) -> Any: get_attr = super().__getattribute__ set_attr = super().__setattr__ nocache = get_attr('_Lazy__nocache') try: if not nocache: return get_attr("_Lazy__result") except AttributeError: pass try: callable_ = get_attr("_Lazy__callable") args = get_attr("_Lazy__args") kwargs = get_attr("_Lazy__kwargs") maxsize = Lazy._max_size key = (callable_, args, tuple(sorted(kwargs.items()))) cache = Lazy._global_cache if not nocache and key in cache: result = cache.pop(key) cache[key] = result else: result = callable_(*args, **kwargs) cache[key] = result while len(cache) > maxsize: cache.popitem(last=False) set_attr("_Lazy__result", result) return result except Exception as e: raise LazyError(f"Failed to compute or cache the result: {e}")
[docs] def __await__(self): return self.getresult().__await__()
[docs] def __aiter__(self): return self.getresult().__aiter__()
[docs] async def __anext__(self): return await self.getresult().__anext__()
[docs] def __enter__(self): return self.getresult().__enter__()
[docs] def __exit__(self, exc_type, exc_val, exc_tb): return self.getresult().__exit__(exc_type, exc_val, exc_tb)
[docs] async def __aenter__(self): return await self.getresult().__aenter__()
[docs] async def __aexit__(self, exc_type, exc_val, exc_tb): return await self.getresult().__aexit__(exc_type, exc_val, exc_tb)
[docs] class LiveResult(Lazy): """ Computes the latest result of a callable. """ def __init__(self, callable_: Callable, *args, **kwargs): super().__init__(callable_, True, *args, **kwargs)
# Auto-forward common dunder methods _dunder_forward = [ "__len__", "__str__", "__repr__", "__bytes__", "__iter__", "__reversed__", "__contains__", "__getitem__", "__setitem__", "__delitem__", "__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__", "__add__", "__sub__", "__mul__", "__matmul__", "__truediv__", "__floordiv__", "__mod__", "__divmod__", "__pow__", "__lshift__", "__rshift__", "__and__", "__xor__", "__or__", "__radd__", "__rsub__", "__rmul__", "__rmatmul__", "__rtruediv__", "__rfloordiv__", "__rmod__", "__rdivmod__", "__rpow__", "__rlshift__", "__rrshift__", "__rand__", "__rxor__", "__ror__", "__index__" ] skip_dunders = {"__repr__"} for name in _dunder_forward: if name in skip_dunders: continue def make_dunder(name): def method(self, *args, **kwargs): try: return getattr(self.getresult(), name)(*args, **kwargs) except AttributeError as e: raise LazyError(f"Attribute '{name}' not found on the lazy result: {e}") except Exception as e: raise LazyError(f"Error in dunder '{name}': {e}") method.__name__ = name return method setattr(Lazy, name, make_dunder(name))