"""
A module implementing a generic, two-way mapping interface.
"""
# built-in
from typing import Generic as _Generic
from typing import Iterator
from typing import MutableMapping as _MutableMapping
from typing import Optional as _Optional
from typing import TypeVar as _TypeVar
from typing import Union as _Union
from typing import cast as _cast
# third-party
from vcorelib.logging import LoggerMixin
from vcorelib.names import name_search
# internal
from runtimepy.mixins.regex import RegexMixin as _RegexMixin
# This determines types that are valid as keys.
T = _TypeVar("T", int, bool)
KeyToName = _MutableMapping[T, str]
NameToKey = _MutableMapping[str, T]
MappingKey = _Union[str, T]
IntMapping = _TypeVar("IntMapping", bound="TwoWayNameMapping[int]")
BoolMapping = _TypeVar("BoolMapping", bound="TwoWayNameMapping[bool]")
IntMappingData = _MutableMapping[MappingKey[int], MappingKey[int]]
BoolMappingData = _MutableMapping[MappingKey[bool], MappingKey[bool]]
EnumMappingData = _Union[
IntMappingData,
BoolMappingData,
_MutableMapping[str, bool],
_MutableMapping[str, int],
]
DEFAULT_PATTERN = ".*"
[docs]
class TwoWayNameMapping(_RegexMixin, LoggerMixin, _Generic[T]):
"""A class interface for managing two-way mappings."""
def __init__(
self,
mapping: KeyToName[T] = None,
reverse: NameToKey[T] = None,
) -> None:
"""Initialize this name registry."""
self._mapping: dict[T, str] = {}
self._reverse: dict[str, T] = {}
self.registered_order: list[str] = []
LoggerMixin.__init__(self)
if mapping is not None:
self.load_key_to_name(mapping)
if reverse is not None:
self.load_name_to_key(reverse)
def _set(self, key: T, name: str) -> None:
"""Set a key<->name pairing."""
assert self.validate_name(name), f"Invalid name '{name}'!"
# Add to the key->name mapping.
if key not in self._mapping:
self._mapping[key] = name
else:
# Ensure the mappings are consistent.
curr_name = self._mapping[key]
assert curr_name == name, f"{curr_name} != {name} ({key})"
# Add to the name->key mapping.
if name not in self._reverse:
self._reverse[name] = key
self.registered_order.append(name)
else:
# Ensure the mappings are consistent.
curr_key = self._reverse[name]
assert curr_key == key, f"{curr_key} != {key} ({name})"
[docs]
def load_key_to_name(self, mapping: KeyToName[T]) -> None:
"""Load a key-to-name mapping."""
for key, name in mapping.items():
self._set(key, name)
[docs]
def load_name_to_key(self, reverse: NameToKey[T]) -> None:
"""Load a name-to-key mapping."""
for name, key in reverse.items():
self._set(key, name)
[docs]
def identifier(self, key: MappingKey[T]) -> _Optional[T]:
"""Get the integer identifier associated with a registry key."""
if isinstance(key, str):
return self._reverse.get(key)
if key in self._mapping:
return key
return None
[docs]
def name(self, key: MappingKey[T]) -> _Optional[str]:
"""Get the name associated with a registry key."""
if isinstance(key, str):
if key in self._reverse:
return key
return self._mapping.get(_cast(T, key))
@property
def names(self) -> Iterator[str]:
"""Iterate over names."""
yield from self._reverse
[docs]
def asdict(self) -> dict[str, T]:
"""Provide a dictionary representation."""
return self._reverse
[docs]
@classmethod
def int_from_dict(
cls: type[IntMapping], data: IntMappingData
) -> IntMapping:
"""
Create an integer-to-name mapping from a dictionary with arbitrary
data.
"""
mapping: KeyToName[int] = {}
reverse: NameToKey[int] = {}
# Set forward and reverse mapping values for the constructor.
for key, value in data.items():
if isinstance(key, str):
reverse[key] = int(value)
else:
mapping[key] = str(value)
return cls(mapping=mapping, reverse=reverse)
[docs]
def search(self, pattern: str, exact: bool = False) -> Iterator[str]:
"""Get names in this mapping based on a pattern."""
yield from name_search(self.names, pattern, exact=exact)
[docs]
@classmethod
def bool_from_dict(
cls: type[BoolMapping], data: BoolMappingData
) -> BoolMapping:
"""
Create a boolean-to-name mapping from a dictionary with arbitrary data.
"""
mapping: KeyToName[bool] = {}
reverse: NameToKey[bool] = {}
# Set forward and reverse mapping values for the constructor.
for key, value in data.items():
if isinstance(key, str):
reverse[key] = bool(value)
else:
mapping[key] = str(value)
return cls(mapping=mapping, reverse=reverse)