Source code for rics.mapping._directional_mapping

from typing import Any, Dict, Generic, Hashable, Iterable, Mapping, Optional, Tuple, TypeVar

from rics.mapping._cardinality import Cardinality
from rics.mapping.exceptions import CardinalityError
from rics.mapping.types import HL, HR, LeftToRight, RightToLeft
from rics.utility.misc import tname

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


[docs]class DirectionalMapping(Generic[HL, HR]): """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: Cardinality.ParseType = None, left_to_right: Mapping[HL, Iterable[HR]] = None, right_to_left: Mapping[HR, Iterable[HL]] = 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, backup_side: Mapping[Any, Iterable[Any]] = 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") from collections import defaultdict other_side = defaultdict(list) for k, matches_for_k in backup_side.items(): for m in matches_for_k: other_side[m].append(k) return {k: tuple(set(m)) for k, m in other_side.items()} def _verify(self, expected: "DirectionalMapping[HL, HR]") -> None: # TODO; This should only be done during testing I think 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) # pragma: no cover @classmethod def _handle_cardinality( cls, expected: Optional[Cardinality.ParseType], left: LeftToRight[HL, HR], right: RightToLeft[HR, HL], verify: bool, ) -> Cardinality: if not (left and right): if expected is None: return Cardinality.ManyToMany # pragma: no cover 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} " f"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}