from __future__ import annotations
from typing import (
TYPE_CHECKING,
Collection,
Mapping,
TypeVar,
)
if TYPE_CHECKING:
from typing import Any, Callable, Iterator
from ridgeplot._types import Numeric
[docs]
def normalise_min_max(val: Numeric, min_: Numeric, max_: Numeric) -> float:
if max_ <= min_:
raise ValueError(
f"max_ should be greater than min_. Got max_={max_} and min_={min_} instead."
)
if not (min_ <= val <= max_):
raise ValueError(f"val ({val}) is out of bounds ({min_}, {max_}).")
return float((val - min_) / (max_ - min_))
[docs]
def get_collection_array_shape(arr: Collection[Any]) -> tuple[int | set[int], ...]:
"""Return the shape of a :class:`~typing.Collection` array.
Parameters
----------
arr
The :class:`~typing.Collection` array.
Returns
-------
Tuple[Union[int, Set[int]], ...]
The elements of the shape tuple give the lengths of the corresponding
array dimensions. If the length of a dimension is variable, the
corresponding element is a :class:`~set` of the variable lengths.
Otherwise, (if the length of a dimension is fixed), the corresponding
element is an :class:`~int`.
Examples
--------
>>> get_collection_array_shape([1, 2, 3])
(3,)
>>> get_collection_array_shape([[1, 2, 3], [4, 5]])
(2, {2, 3})
>>> get_collection_array_shape(
... [
... [
... [1, 2, 3], [4, 5]
... ],
... [
... [6, 7, 8, 9],
... ],
... ]
... )
(2, {1, 2}, {2, 3, 4})
>>> get_collection_array_shape(
... [
... [
... [1], [2, 3], [4, 5, 6],
... ],
... [
... [7, 8, 9, 10, 11],
... ],
... ]
... )
(2, {1, 3}, {1, 2, 3, 5})
>>> get_collection_array_shape(
... [
... [
... [(0, 0), (1, 1), (2, 2), (3, 3)],
... [(0, 0), (1, 1), (2, 2)],
... [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
... ],
... [
... [(-2, 2), (-1, 1), (0, 1)],
... [(2, 2), (3, 1), (4, 1)],
... ],
... ]
... )
(2, {2, 3}, {3, 4, 5}, 2)
>>> get_collection_array_shape(
... [
... [
... ["a", "b", "c", "d"], ["e", "f"],
... ],
... [
... ["h", "i", "j", "k", "l"],
... ],
... ]
... )
(2, {1, 2}, {2, 4, 5})
"""
def _get_dim_length(obj: Any) -> int:
"""Return the length of a dimension of a :class:`~typing.Collection` array."""
if not isinstance(obj, Collection) or isinstance(obj, str):
raise TypeError(f"Expected a Collection. Got {type(obj)} instead.")
return len(obj)
shape: list[int | set[int]] = [_get_dim_length(arr)]
while isinstance(arr, Collection):
try:
dim_lengths = set(map(_get_dim_length, arr))
except TypeError:
break
shape.append(dim_lengths.pop() if len(dim_lengths) == 1 else dim_lengths)
arr = [item for sublist in arr for item in sublist]
return tuple(shape)
_KT = TypeVar("_KT") # Mapping key type
_VT = TypeVar("_VT") # Mapping value type
[docs]
class LazyMapping(Mapping[_KT, _VT]):
"""A lazy mapping that loads its contents only when first needed.
Parameters
----------
loader
A callable that returns a mapping.
Examples
--------
>>> def my_io_loader() -> dict[str, int]:
... print("Loading...")
... return {"a": 1, "b": 2}
...
>>> lazy_mapping = LazyMapping(my_io_loader)
>>> lazy_mapping
Loading...
{'a': 1, 'b': 2}
"""
__slots__ = ("_loader", "_inner_mapping")
def __init__(self, loader: Callable[[], Mapping[_KT, _VT]]):
self._loader = loader
self._inner_mapping: Mapping[_KT, _VT] | None = None
@property
def _mapping(self) -> Mapping[_KT, _VT]:
if self._inner_mapping is None:
self._inner_mapping = self._loader()
return self._inner_mapping
def __getitem__(self, item: _KT) -> _VT:
return self._mapping.__getitem__(item)
def __iter__(self) -> Iterator[_KT]:
return self._mapping.__iter__()
def __len__(self) -> int:
return self._mapping.__len__()
def __str__(self) -> str:
return self._mapping.__str__()
def __repr__(self) -> str:
return self._mapping.__repr__()