Source code for rics.mapping._directional_mapping

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

from rics.cardinality import Cardinality, CardinalityType
from rics.cardinality.exceptions import CardinalityError
from rics.utility.misc import tname

HL = TypeVar("HL", bound=Hashable)
HR = TypeVar("HR", bound=Hashable)
HAnySide = TypeVar("HAnySide", bound=Hashable)
MatchTupleLeft = Tuple[HL, ...]
MatchTupleRight = Tuple[HR, ...]
MatchTupleAnySide = TypeVar("MatchTupleAnySide", bound=Hashable)
DictMapping = Union[Dict[HL, MatchTupleRight], Dict[HR, MatchTupleLeft]]


[docs]class DirectionalMapping(Generic[HL, HR]): """A two-way mapping between hashable elements. Args: cardinality: Explicit cardinality. None=derive. 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, left_to_right: DictMapping = None, right_to_left: DictMapping = None, _verify: bool = True, ) -> None: self._left_to_right = self._to_other(left_to_right, right_to_left) self._right_to_left = 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) -> MatchTupleLeft: """Left-side elements in the mapping.""" return tuple(self._left_to_right) @property def right(self) -> MatchTupleRight: """Right-side elements in the mapping.""" return tuple(self._right_to_left) @property def left_to_right(self) -> DictMapping: """Left-to-right element mappings.""" return self._left_to_right @property def right_to_left(self) -> DictMapping: """Right-to-left element mappings.""" return self._right_to_left @property def reverse(self) -> "DirectionalMapping": """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 :class:`~rics.cardinality.Cardinality.OneToOne`. """ if self._cardinality != Cardinality.OneToOne: raise CardinalityError(f"Must have {Cardinality.OneToOne}.", self._cardinality) # pragma: no cover 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": """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 self._select(elements, left=True, exclude=exclude)
[docs] def select_right(self, elements: Iterable[HR], exclude: bool = False) -> "DirectionalMapping": """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 self._select(elements, left=False, exclude=exclude)
@staticmethod def _to_other(primary_side: Optional[DictMapping], backup_side: Optional[DictMapping]) -> DictMapping: if primary_side is not None: return primary_side 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 _select(self, elements: Iterable[HAnySide], left: bool, exclude: bool) -> "DirectionalMapping": """Perform a selection on left-side elements. Args: elements: Elements to select. left: If True, select elements from the left side. 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``. """ items = self._left_to_right if left else self._right_to_left if not exclude: missing_elements = set(elements).difference(items) if missing_elements: raise KeyError(f"Unknown {'left' if left else 'right'}: {', '.join(map(str, missing_elements))}.") s = set(elements) chosen_elements = filter(lambda e: e not in s, items) if exclude else filter(s.__contains__, items) one_sided_mapping = {e: items[e] for e in chosen_elements} return ( DirectionalMapping(None, left_to_right=one_sided_mapping) if left else DirectionalMapping(None, right_to_left=one_sided_mapping) ) def _verify(self, expected: "DirectionalMapping") -> 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})" @classmethod def _handle_cardinality( cls, expected: Optional[CardinalityType], left: DictMapping, right: DictMapping, verify: bool, ) -> Cardinality: if not (left and right): if expected is None: raise ValueError("Explicit cardinality must be given for empty mapping.") # 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}.", actual) 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}.")