Source code for lembas.results

from __future__ import annotations

import weakref
from functools import cached_property
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import TypeVar

if TYPE_CHECKING:
    from lembas.case import Case


TCase = TypeVar("TCase", bound="Case")
RawCaseMethod = Callable[[TCase], Any]


[docs] class Results: """A generic container for results of a case. Implements lazy loading of results, where the result accessors are specified by @result decorator. """ def __init__(self, parent: Case): self._parent = weakref.ref(parent) @cached_property def parent(self) -> Case: """A reference to the parent case with which these results are associated.""" parent = self._parent() if parent is None: # pragma: no cover raise ValueError( "The parent has been de-referenced. This shouldn't happen." ) return parent def __getattr__(self, item: str) -> Any: # During attribute access, we search the class for methods to which have been # attached a "_provides_results" tuple. If we find that, and the requested # result is in that tuple, we call the method (once) and cache the results in # the self.__dict__ for later, faster retrieval. cls = self.parent.__class__ for method_name, method_func in cls.__dict__.items(): try: provides_results = getattr(method_func, "_provides_results") except AttributeError: continue # to next method provides_results = provides_results or tuple() if item not in provides_results: continue results = method_func(self.parent) if not isinstance(results, tuple): results = (results,) num_expected_results = len(provides_results) num_results = len(results) if num_expected_results != num_results: raise ValueError( f"Results method {method_name} returns {num_results} items, " f"only {num_expected_results} results are declared in the @result " "decorator." ) for n, r in zip(provides_results, results): setattr(self, n, r) try: return self.__dict__[item] except KeyError: raise AttributeError(f"Result '{item}' is not defined")
[docs] def get(self, item: str, default: Any = None) -> Any: """Dictionary-like get access.""" return getattr(self, item, default)
[docs] def result( *func_or_names: Callable[[TCase], Any] | str ) -> RawCaseMethod | Callable[[RawCaseMethod], RawCaseMethod]: """A decorator to annotate a method that provides result(s). The decorator accepts a variadic list of names for the provided result(s). The method can return a single object or a tuple of objects, which must map to the number of names provided. The results are then available from within other case handler methods like self.results.result_name. """ if any(callable(fn) for fn in func_or_names): # This case captures the non-argument form, i.e. @result # In this case, there should only be one argument, which is # the decorated method. try: (method,) = func_or_names except ValueError: # pragma: no cover raise ValueError("Must only provide a single callable") names = (method.__name__,) # type: ignore method._provides_results = names # type: ignore return method # type: ignore # We now handle the case with arguments, i.e. @result("name1", "name2") names = func_or_names # type: ignore def decorator(m: RawCaseMethod) -> RawCaseMethod: # Here, we attach the tuple of names to the method function object. We have to do # this because we do not have access to the class object at the time when the # decorator is called. The actual discovery of the name mapping is performed # during attribute access on the case.results object, which does a search across # the methods attached to the class at runtime. m._provides_results = names # type: ignore return m return decorator