Coverage for src / loman / util.py: 100%
68 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-22 21:30 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-22 21:30 +0000
1"""Utility functions and classes for loman computation graphs."""
3import itertools
4import types
5from collections.abc import Callable, Generator, Iterable
6from typing import Any, TypeVar
8import numpy as np
9import pandas as pd
11T = TypeVar("T")
12R = TypeVar("R")
15def apply1(
16 f: Callable[..., R], xs: T | list[T] | Generator[T, None, None], *args: Any, **kwds: Any
17) -> R | list[R] | Generator[R, None, None]:
18 """Apply function f to xs, handling generators, lists, and single values."""
19 if isinstance(xs, types.GeneratorType):
20 return (f(x, *args, **kwds) for x in xs)
21 if isinstance(xs, list):
22 return [f(x, *args, **kwds) for x in xs]
23 return f(xs, *args, **kwds)
26def as_iterable(xs: T | Iterable[T]) -> Iterable[T]:
27 """Convert input to iterable form if not already iterable."""
28 if isinstance(xs, (types.GeneratorType, list, set)):
29 return xs # type: ignore[return-value]
30 return (xs,) # type: ignore[return-value]
33def apply_n(f: Callable[..., Any], *xs: Any, **kwds: Any) -> None:
34 """Apply function f to the cartesian product of iterables xs."""
35 for p in itertools.product(*[as_iterable(x) for x in xs]):
36 f(*p, **kwds)
39class AttributeView:
40 """Provides attribute-style access to dynamic collections."""
42 def __init__(
43 self,
44 get_attribute_list: Callable[[], Iterable[str]],
45 get_attribute: Callable[[str], Any],
46 get_item: Callable[[Any], Any] | None = None,
47 ) -> None:
48 """Initialize with functions to get attribute list and individual attributes.
50 Args:
51 get_attribute_list: Function that returns list of available attributes
52 get_attribute: Function that takes an attribute name and returns its value
53 get_item: Optional function for item access, defaults to get_attribute
54 """
55 self.get_attribute_list = get_attribute_list
56 self.get_attribute = get_attribute
57 self.get_item: Callable[[Any], Any] = get_item if get_item is not None else get_attribute
59 def __dir__(self) -> list[str]:
60 """Return list of available attributes."""
61 return list(self.get_attribute_list())
63 def __getattr__(self, attr: str) -> Any:
64 """Get attribute by name, raising AttributeError if not found."""
65 try:
66 return self.get_attribute(attr)
67 except KeyError as e:
68 raise AttributeError(attr) from e
70 def __getitem__(self, key: Any) -> Any:
71 """Get item by key."""
72 return self.get_item(key)
74 def __getstate__(self) -> dict[str, Any]:
75 """Prepare object for serialization."""
76 return {
77 "get_attribute_list": self.get_attribute_list,
78 "get_attribute": self.get_attribute,
79 "get_item": self.get_item,
80 }
82 def __setstate__(self, state: dict[str, Any]) -> None:
83 """Restore object from serialized state."""
84 self.get_attribute_list = state["get_attribute_list"]
85 self.get_attribute = state["get_attribute"]
86 self.get_item = state["get_item"]
87 if self.get_item is None:
88 self.get_item = self.get_attribute
90 @staticmethod
91 def from_dict(d: dict[Any, Any], use_apply1: bool = True) -> "AttributeView":
92 """Create an AttributeView from a dictionary."""
93 if use_apply1:
95 def get_attribute(xs: Any) -> Any:
96 """Get attribute value from dictionary with apply1 support."""
97 return apply1(d.get, xs)
98 else:
99 get_attribute = d.get
100 return AttributeView(d.keys, get_attribute)
103pandas_types = (pd.Series, pd.DataFrame)
106def value_eq(a: Any, b: Any) -> bool:
107 """Compare two values for equality, handling pandas and numpy objects safely.
109 - Uses .equals for pandas Series/DataFrame
110 - For numpy arrays, returns a single boolean using np.array_equal (treats NaNs as equal)
111 - Falls back to == and coerces to bool when possible
112 """
113 if a is b:
114 return True
116 # pandas objects: use robust equality
117 if isinstance(a, pandas_types):
118 return bool(a.equals(b))
119 if isinstance(b, pandas_types): # pragma: no cover
120 return bool(b.equals(a))
121 if isinstance(a, np.ndarray) or isinstance(b, np.ndarray):
122 try:
123 return bool(np.array_equal(a, b, equal_nan=True))
124 except Exception:
125 return False
127 # Default comparison; ensure a single boolean
128 try:
129 result = a == b
130 # If result is an array-like truth value, reduce safely
131 if isinstance(result, (np.ndarray,)):
132 return bool(np.all(result))
133 return bool(result)
134 except Exception:
135 return False