from collections.abc import MutableMapping
from typing import Any, Dict, Iterator
# pyFuRNAce IMPORTS
from .callback import Callback
[docs]
class BasePair(MutableMapping, Callback):
"""
A bidirectional dictionary that maintains a mapping between keys and values,
ensuring that each value has a unique corresponding key and vice versa.
Inherits from MutableMapping to provide dictionary-like behavior and from
Callback to support event-driven notifications on updates.
Parameters
----------
*args : tuple
Positional arguments passed to the internal dictionary.
**kwargs : dict
Keyword arguments passed to the internal dictionary. Special keys
`callback` and `callbacks` are removed before processing.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
Callback.__init__(self, **kwargs)
kwargs.pop("callback", None)
kwargs.pop("callbacks", None)
self._store: Dict[Any, Any] = dict(*args, **kwargs)
self._reverse: Dict[Any, Any] = {v: k for k, v in self._store.items()}
self._callbacks = []
def __getitem__(self, key: Any) -> Any:
"""
Retrieve the value associated with the given key.
Parameters
----------
key : hashable
The key or value to retrieve its counterpart.
Returns
-------
Any
The corresponding value or key.
Raises
------
KeyError
If the key is not found.
"""
if key in self._store:
return self._store[key]
if key in self._reverse:
return self._reverse[key]
raise KeyError(key)
def __setitem__(self, key: Any, value: Any) -> None:
"""
Set a key-value pair in the dictionary, maintaining bidirectionality.
Parameters
----------
key : hashable
The key to set.
value : hashable
The corresponding value.
"""
if key in self._store:
del self._reverse[self._store[key]]
elif key in self._reverse:
del self._store[self._reverse[key]]
self._store[key] = value
self._reverse[value] = key
self._trigger_callbacks()
def __delitem__(self, key: Any) -> None:
"""
Delete a key-value pair from the dictionary.
Parameters
----------
key : hashable
The key to remove.
"""
value = self._store[key]
del self._store[key]
del self._reverse[value]
self._trigger_callbacks()
def __str__(self) -> str:
"""Return a string representation of the dictionary."""
return str(self._store)
def __repr__(self) -> str:
"""Return a string representation of the dictionary."""
return str(self._store)
def __contains__(self, key: Any) -> bool:
"""Check if the key or value exists in the dictionary."""
return key in self._store or key in self._reverse
def __iter__(self) -> Iterator[Any]:
"""Return an iterator over the keys of the dictionary."""
return iter(self._store)
def __len__(self) -> int:
"""Return the number of key-value pairs in the dictionary."""
return len(self._store)
def __eq__(self, other: Any) -> bool:
"""Check if two Basepair or dictionary have the same key-value pairs."""
if not isinstance(other, (BasePair, dict)):
return False
return all(self[k] == other.get(k) for k in self.keys()) and all(
other[k] == self.get(k) for k in other.keys()
)
###
### PUBLIC METHODS
###
[docs]
def keys(self) -> Any:
"""Return the dictionary's keys."""
return self._store.keys()
[docs]
def values(self) -> Any:
"""Return the dictionary's values."""
return self._store.values()
[docs]
def items(self) -> Any:
"""Return the dictionary's key-value pairs."""
return self._store.items()
[docs]
def update(self, *args: Dict[Any, Any], **kwargs: Any) -> "BasePair":
"""
Update the dictionary with new key-value pairs.
Parameters
----------
*args : dict
Dictionaries to merge into the current instance.
**kwargs : dict
Additional key-value pairs to update.
Returns
-------
BasePair
The updated instance.
"""
for new_dict in args:
for k, v in new_dict.items():
self[k] = v
for k, v in kwargs.items():
self[k] = v
self._trigger_callbacks()
return self
[docs]
def get(self, key: Any, default: Any = None) -> Any:
"""
Retrieve a value by key, returning a default if not found.
Parameters
----------
key : hashable
The key to retrieve.
default : Any, optional
The value to return if key is not found (default is None).
Returns
-------
Any
The corresponding value or the default.
"""
if key in self._store:
return self._store[key]
elif key in self._reverse:
return self._reverse[key]
return default
[docs]
def copy(self, **kwargs: Any) -> "BasePair":
"""
Create a copy of the dictionary.
Parameters
----------
**kwargs : dict
Additional parameters for the new instance.
Returns
-------
BasePair
A new instance of BasePair with the same data.
"""
new_instance = BasePair(self._store, **kwargs)
return new_instance
[docs]
def shift(self, shift: Any) -> "BasePair":
"""
Apply a shift transformation to the key-value pairs.
The shift is added to both the keys and values.
Parameters
----------
shift : hashable
The amount to shift each key and value.
Returns
-------
BasePair
A new instance with shifted keys and values.
"""
new_bp_dict = BasePair()
for key, val in self.items():
new_bp_dict[key + shift] = val + shift
return new_bp_dict