__all__ = ["ReadonlyDict"]
__version__ = "1.0.0"
# standard library
from collections.abc import Iterable, Iterator, Mapping
from typing import TYPE_CHECKING, Any, TypeVar, overload
# dependencies
from typing_extensions import Self
# type hints
K = TypeVar("K")
V = TypeVar("V")
K2 = TypeVar("K2")
V2 = TypeVar("V2")
Tuples = Iterable[tuple[K, V]]
[docs]
class ReadonlyDict(Mapping[K, V]):
"""Drop-in read-only dictionary with typing and runtime compatibility.
- ``ReadonlyDict()`` -> New empty read-only dictionary.
- ``ReadonlyDict(mapping)`` -> New read-only dictionary
initialized from a mapping object's ``(key, value)`` pairs.
- ``ReadonlyDict(iterable)`` -> New read-only dictionary
initialized as if via: ``d = {}; for k, v in iterable: d[k] = v``.
- ``ReadonlyDict(**kwargs)`` -> New read-only dictionary
initialized with the ``name=value`` pairs in the keyword argument list.
For example: ``ReadonlyDict(one=1, two=2)``.
"""
_data: dict[K, V]
_hash: int | None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# methods for initialization
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if TYPE_CHECKING:
@overload
def __new__(cls, mapping: Mapping[K, V], /) -> Self: ... # pyright: ignore
@overload
def __new__(cls, iterable: Tuples[K, V], /) -> Self: ... # pyright: ignore
@overload
def __new__(cls, **kwargs: V) -> "ReadonlyDict[str, V]": ...
# fmt: off
@overload
def __new__(cls, mapping: Mapping[K, V], /, **kwargs: V2) -> "ReadonlyDict[K | str, V | V2]": ...
@overload
def __new__(cls, iterable: Tuples[K, V], /, **kwargs: V2) -> "ReadonlyDict[K | str, V | V2]": ...
# fmt: on
def __new__(cls, *args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
return super().__new__(cls)
else:
[docs]
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize ``self``. See ``help(type(self))`` for accurate signature."""
self._data = dict(*args, **kwargs)
self._hash = None
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# methods for immutability
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def __copy__(self) -> Self:
"""Return ``self`` for a shallow copy."""
return self
[docs]
def __hash__(self) -> int:
"""Return ``hash(self)``."""
if self._hash is None:
self._hash = hash(frozenset(self._data.items()))
return self._hash
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# methods for instantiation
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[docs]
def __getitem__(self, key: K, /) -> V:
"""Return ``self[key]``."""
return self._data[key]
[docs]
def __iter__(self) -> Iterator[K]:
"""Return ``iter(self).``"""
return iter(self._data)
[docs]
def __len__(self) -> int:
"""Return ``len(self)``."""
return len(self._data)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# methods to be compatible with built-in dictionary
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[docs]
def copy(self) -> Self:
"""Return a shallow copy of ``self``."""
return self
# fmt: off
@overload
@classmethod
def fromkeys(cls, iterable: Iterable[K2], /) -> "ReadonlyDict[K2, None]": ...
@overload
@classmethod
def fromkeys(cls, iterable: Iterable[K2], value: V2, /) -> "ReadonlyDict[K2, V2]": ...
# fmt: on
[docs]
@classmethod
def fromkeys(cls, iterable: Iterable[Any], value: Any = None, /) -> Any:
"""Create a new read-only dictionary with keys from ``iterable`` and values set to ``value``."""
return cls(dict.fromkeys(iterable, value))
[docs]
def __or__(self, other: Mapping[K2, V2], /) -> "ReadonlyDict[K | K2, V | V2]":
"""Return ``self|value``."""
if not isinstance(other, Mapping): # pyright: ignore
return NotImplemented
return self.__class__(self._data | dict(other)) # pyright: ignore
[docs]
def __ror__(self, other: Mapping[K2, V2], /) -> dict[K | K2, V | V2]:
"""Return ``value|self``."""
if not isinstance(other, Mapping): # pyright: ignore
return NotImplemented
return dict(other) | self._data
[docs]
def __repr__(self) -> str:
"""Return ``repr(self)``."""
return f"{self.__class__.__name__}({self._data!r})"
[docs]
def __reversed__(self) -> Iterator[K]:
"""Return ``reversed(self)``."""
return reversed(self._data)