objtools/objtools/collections.py

117 lines
3.7 KiB
Python

"""
This module provides helpers for working with collections (as in abstract
base classes defined in the ``collections.abc`` standard Python module:
iterables, iterators, mappings, etc.).
"""
from typing import (
Mapping, MutableMapping, Sequence, Set,
Iterable, Iterator, ByteString, Any,
)
def namespacify(value: Any) -> Any:
"""
Turn all mappings into :class:`Namespace` instances as needed.
:param value: Anything that may be a mapping or may be an iterable
that may hold a mapping.
:returns:
* The value itself, if it is a Namespace instance or is neither
a mapping, a sequence, a set, or an iterable;
* A :class:`Namespace` instance for a mapping;
* A list holding the result of calling :meth:`namespacify` on
every item for a sequence;
* A set holding the result of calling :meth:`namespacify` on
every item for a set;
* A ``map`` object applying :meth:`namespacify` on every item
for other iterables.
"""
if isinstance(value, Namespace):
return value
if isinstance(value, Mapping):
return Namespace(value)
if isinstance(value, (str, ByteString)):
# Do not treat strings and bytestrings as normal sequences
return value
if isinstance(value, Sequence):
return list(map(namespacify, value))
if isinstance(value, Set):
return set(map(namespacify, value))
if isinstance(value, Iterable):
return map(namespacify, value)
return value
class Namespace(MutableMapping):
"""
A class that maps items of a mapping to attributes. Takes the same
arguments as a classic ``dict``. Attributes and items are kept in sync;
deleting an item deletes the attribute, and updating an item updates the
attribute, and vice-versa.
Getting an attribute will call :meth:`namespacify` on the returned value,
to allow easier manipulation of complex structures such as JSON documents:
.. code:: python
>>> from objtools.collections import Namespace
>>> ns = Namespace({'foo': [{'bar': {'baz': 42}}]})
>>> ns.foo[0].bar.baz
42
"""
def __init__(self, *args: Iterable, **kwargs: Any):
"""
:param \\*args: Mappings or iterables yielding
``(key, value)`` two-tuples.
:param \\**kwargs: Key-value pairs set as attributes.
"""
for iterable in (*args, kwargs):
if isinstance(iterable, Mapping):
iterable = iterable.items()
for k, v in iterable:
setattr(self, k, v)
def __getitem__(self, name: Any) -> Any:
try:
attr = getattr(self, name)
except AttributeError:
raise KeyError(repr(name))
return namespacify(attr)
def __setitem__(self, name: Any, value: Any) -> None:
setattr(self, name, value)
def __delitem__(self, name: Any) -> None:
try:
delattr(self, name)
except AttributeError:
raise KeyError(repr(name))
def __iter__(self) -> Iterator:
return iter(self.__dict__)
def __len__(self) -> int:
return len(self.__dict__)
def __repr__(self) -> str:
return '{}({!r})'.format(self.__class__.__name__, self.__dict__)
def __str__(self) -> str:
return str(self.__dict__)
def _repr_pretty_(self, p: Any, cycle: bool) -> None:
# IPython's pretty printing
if cycle:
p.text('{}(...)'.format(self.__class__.__name__))
else:
p.text('{}('.format(self.__class__.__name__))
p.pretty(self.__dict__)
p.text(')')
def copy(self) -> 'Namespace':
"""
Create a shallow copy of this namespace.
"""
return self.__class__(self.__dict__)