Source code for id_translation.mapping._directional_mapping

from collections.abc import Hashable, Iterable, Mapping
from typing import Any, Generic, TypeVar

from rics.misc import tname

from ._cardinality import Cardinality
from .exceptions import CardinalityError
from .types import HL, HR, CardinalityType, LeftToRight, RightToLeft

HAnySide = TypeVar("HAnySide", bound=Hashable)
MatchTupleAnySide = TypeVar("MatchTupleAnySide", bound=Hashable)  # TODO: Higher-Kinded TypeVars


[docs] class DirectionalMapping(Generic[HL, HR]): # noqa: PLW1641 """A two-way mapping between hashable elements. Args: cardinality: Explicit cardinality. Derive if ``None``. left_to_right: A left-to-right mapping of elements. right_to_left: A right-to-left mapping of elements. _verify: If ``False``, input checks are disabled. Intended for internal use. Raises: ValueError: If both of `left_to_right` and `right_to_left` are ``None``. ValueError: If verification of two-sided input fails, and ``verify=True``. CardinalityError: If explicit `cardinality` < :attr:`cardinality`, and ``verify=True``. """ def __init__( self, cardinality: CardinalityType | None = None, left_to_right: Mapping[HL, Iterable[HR]] | None = None, right_to_left: Mapping[HR, Iterable[HL]] | None = None, _verify: bool = True, ) -> None: self._left_to_right: dict[HL, tuple[HR, ...]] = self._to_other(left_to_right, right_to_left) self._right_to_left: dict[HR, tuple[HL, ...]] = self._to_other(right_to_left, left_to_right) if left_to_right is not None and right_to_left is not None and _verify: self._verify(expected=DirectionalMapping(cardinality, left_to_right=left_to_right, _verify=False)) self._cardinality = self._handle_cardinality(cardinality, self._left_to_right, self._right_to_left, _verify) @property def cardinality(self) -> Cardinality: """Cardinality with which this mapping was created. Returns: Cardinality with which this mapping was created. """ return self._cardinality @property def left(self) -> tuple[HL, ...]: """Left-side elements in the mapping.""" return tuple(self._left_to_right) @property def right(self) -> tuple[HR, ...]: """Right-side elements in the mapping.""" return tuple(self._right_to_left) @property def left_to_right(self) -> LeftToRight[HL, HR]: """Left-to-right element mappings.""" return self._left_to_right @property def right_to_left(self) -> RightToLeft[HR, HL]: """Right-to-left element mappings.""" return self._right_to_left @property def reverse(self) -> "DirectionalMapping[HR, HL]": """Reverse the mapping by swapping the sides. Returns: A copy with data identical to the calling instance, but with sides inversed compared to the caller. """ return DirectionalMapping( self.cardinality.inverse, left_to_right=self._right_to_left.copy(), right_to_left=self._left_to_right.copy(), _verify=False, )
[docs] def flatten(self) -> dict[HL, HR]: """Return a flattened version of self as a dict. Returns: A dict ``{left: right}``. Raises: CardinalityError: If cardinality is not :attr:`~.Cardinality.OneToOne` or :attr:`~.Cardinality.ManyToOne`. """ if not self._cardinality.one_right: # pragma: no cover raise CardinalityError(f"Must have one of {(Cardinality.OneToOne, Cardinality.ManyToOne)} to flatten.") return {left: right[0] for left, right in self._left_to_right.items()}
[docs] def select_left(self, elements: Iterable[HL], exclude: bool = False) -> "DirectionalMapping[HL, HR]": """Perform a selection on left-side elements. Args: elements: Elements to select. exclude: If ``True``, return everything **except** the given elements. Returns: A new Mapping for the selection. Raises: KeyError: If any of the chosen elements do not exist and ``exclude=False``. """ return DirectionalMapping(None, left_to_right=_select(elements, self._left_to_right, exclude))
[docs] def select_right(self, elements: Iterable[HR], exclude: bool = False) -> "DirectionalMapping[HL, HR]": """Perform a selection on right-side elements. Args: elements: Elements to select. exclude: If ``True``, return everything **except** the given elements. Returns: A new instance for the selection. Raises: KeyError: If any of the chosen elements do not exist and ``exclude=False``. """ return DirectionalMapping(None, right_to_left=_select(elements, self._right_to_left, exclude))
@classmethod def _to_other( cls, primary_side: Mapping[Any, Iterable[Any]] | None = None, backup_side: Mapping[Any, Iterable[Any]] | None = None, ) -> dict[Any, Any]: if primary_side is not None: return primary_side # type: ignore[return-value] if backup_side is None: raise ValueError("At least one side must be given") other_side: dict[Any, list[Any]] = {} for k, matches_for_k in backup_side.items(): for m in matches_for_k: other_side.setdefault(m, []).append(k) return {k: tuple(set(m)) for k, m in other_side.items()} def _verify(self, expected: "DirectionalMapping[HL, HR]") -> None: self._verify_side(self.left, expected.left, "Left") self._verify_side(self.right, expected.right, "Right") def __eq__(self, other: Any) -> bool: if not isinstance(other, DirectionalMapping): return False return ( self._left_to_right == other._left_to_right and self._right_to_left == other._right_to_left and self._cardinality == other._cardinality ) def __repr__(self) -> str: n_left = len(self._left_to_right) n_right = len(self._right_to_left) return f"{tname(self)}({n_left} left | {n_right} right, type={self.cardinality.name})" def __bool__(self) -> bool: return bool(self._left_to_right or self._right_to_left) @classmethod def _handle_cardinality( cls, expected: CardinalityType | None, left: LeftToRight[HL, HR], right: RightToLeft[HR, HL], verify: bool, ) -> Cardinality: if not (left and right): if expected is None: return Cardinality.ManyToMany else: return Cardinality.parse(expected) actual = Cardinality.from_counts( left_count=max(map(len, right.values())), right_count=max(map(len, left.values())) ) if expected is None: return actual else: expected = Cardinality.parse(expected) if verify and actual > expected: raise CardinalityError( f"Cannot cast explicit given type {expected} to actual type {actual} for ({left=} | {right=})", ) return expected @classmethod def _verify_side(cls, actual: MatchTupleAnySide, expected: MatchTupleAnySide, name: str) -> None: if actual != expected: raise ValueError(f"{name}-side mismatch: Got {actual} but expected {expected}.")
def _select(elements: Iterable[HAnySide], items: dict[HL, tuple[HR, ...]], exclude: bool) -> dict[HL, tuple[HR, ...]]: if not exclude: missing_elements = set(elements).difference(items) if missing_elements: raise KeyError(f"Unknown keys {missing_elements} in {items}.") s = set(elements) chosen_elements = filter(lambda e: e not in s, items) if exclude else filter(s.__contains__, items) return {e: items[e] for e in chosen_elements}