from __future__ import annotations
from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping
from typing import Any, Optional
from .types import lower
[docs]
class NormalizedDict(MutableMapping):
"""
A generalization of a case-insensitive dictionary. `NormalizedDict` takes
a callable (the "normalizer") that is applied to any key passed to its
`~object.__getitem__`, `~object.__setitem__`, or `~object.__delitem__`
method, and the result of the call is then used for the actual lookup.
When iterating over a `NormalizedDict`, each key is returned as the
"pre-normalized" form passed to `~object.__setitem__` the last time the key
was set (but see `normalized()` below). Aside from this, `NormalizedDict`
behaves like a normal `~collections.abc.MutableMapping` class.
If a normalizer is not specified upon instantiation, a default will be used
that converts strings to lowercase and leaves everything else unchanged, so
`NormalizedDict` defaults to yet another case-insensitive dictionary.
Two `NormalizedDict` instances compare equal iff their normalizers, bodies,
and `normalized_dict()` return values are equal. When comparing a
`NormalizedDict` to any other type of mapping, the other mapping is first
converted to a `NormalizedDict` using the same normalizer.
:param mapping data: a mapping or iterable of ``(key, value)`` pairs with
which to initialize the instance
:param callable normalizer: A callable to apply to keys before looking them
up; defaults to `lower`. The callable MUST be idempotent (i.e.,
``normalizer(x)`` must equal ``normalizer(normalizer(x))`` for all
inputs) or else bad things will happen to your dictionary.
:param body: initial value for the `body` attribute
:type body: string or `None`
"""
def __init__(
self,
data: None | Mapping | Iterable[tuple[Any, Any]] = None,
normalizer: Optional[Callable[[Any], Any]] = None,
body: Optional[str] = None,
) -> None:
self._data: dict[Any, tuple[Any, Any]] = {}
self.normalizer: Callable[[Any], Any] = (
normalizer if normalizer is not None else lower
)
#: This is where `HeaderParser` stores the message body (if any)
#: accompanying the header section represented by the mapping
self.body: Optional[str] = body
if data is not None:
# Don't call `update` until after `normalizer` is set.
self.update(data)
def __getitem__(self, key: Any) -> Any:
return self._data[self.normalizer(key)][1]
def __setitem__(self, key: Any, value: Any) -> None:
self._data[self.normalizer(key)] = (key, value)
def __delitem__(self, key: Any) -> None:
del self._data[self.normalizer(key)]
def __iter__(self) -> Iterator:
return (key for key, _ in self._data.values())
def __len__(self) -> int:
return len(self._data)
def __eq__(self, other: Any) -> bool:
if isinstance(other, NormalizedDict):
if self.normalizer != other.normalizer or self.body != other.body:
return False
elif isinstance(other, Mapping):
if self.body is not None:
return False
other = NormalizedDict(other, normalizer=self.normalizer)
else:
return NotImplemented
return self.normalized_dict() == other.normalized_dict()
def __repr__(self) -> str:
return (
"{0.__module__}.{0.__name__}"
"({2!r}, normalizer={1.normalizer!r}, body={1.body!r})".format(
type(self), self, dict(self)
)
)
[docs]
def normalized(self) -> NormalizedDict:
"""
Return a copy of the instance such that iterating over it will return
normalized keys instead of the keys passed to `~object.__setitem__`
>>> normdict = NormalizedDict()
>>> normdict['Foo'] = 23
>>> normdict['bar'] = 42
>>> sorted(normdict)
['Foo', 'bar']
>>> sorted(normdict.normalized())
['bar', 'foo']
:rtype: NormalizedDict
"""
return NormalizedDict(
self.normalized_dict(),
normalizer=self.normalizer,
body=self.body,
)
[docs]
def normalized_dict(self) -> dict:
"""
Convert to a `dict` with all keys normalized. (A `dict` with
non-normalized keys can be obtained with ``dict(normdict)``.)
:rtype: dict
"""
return {key: value for key, (_, value) in self._data.items()}
[docs]
def copy(self) -> NormalizedDict:
"""Create a shallow copy of the mapping"""
dup = type(self)()
dup._data = self._data.copy()
dup.normalizer = self.normalizer
dup.body = self.body
return dup