"""Data structure utilities."""
from collections.abc import Iterable, Mapping
def unique_list_order_preserved(seq):
"""Returns a unique list of items from the sequence
while preserving original ordering.
The first occurrence of an item is returned in the new sequence:
any subsequent occurrences of the same item are ignored.
"""
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
[docs]class DotDict(dict):
"""Read-only dict-like object class that wraps a mapping object."""
def __init__(self, *args, **kwargs):
dict.__init__(self)
self.update(*args, **kwargs)
def __repr__(self):
return f"{self.__class__.__name__}({super().__repr__()})"
def __getattr__(self, name):
"""Simply attempts to grab a key `name`.
Has some fallback logic for keys starting with '@' and '#',
which are output by xmltodict when a tag has attributes included.
In that case, will attempt to find a key starting with '@' or '#',
or will raise the original KeyError exception.
"""
try:
return self[name]
except KeyError:
# No key by that name? Let's try being helpful.
if f"@{name}" in self:
# Does this same name occur starting with ``@``?
return self[f"@{name}"]
if f"#{name}" in self:
# Does this same name occur starting with ``#``?
return self[f"#{name}"]
# Otherwise, raise the original exception
raise
def __setattr__(self, name, val):
"""Allows assigning new values to a DotDict, which will automatically build
nested mapping objects into DotDicts, as well.
Passes responsibility to ``__setitem__`` for consistency.
"""
self.__setitem__(name, val)
def __delattr__(self, name):
"""Passes attr deletion to __delitem__."""
self.__delitem__(name)
def __setitem__(self, key, val):
"""Allows assigning new values to a DotDict, which will automatically build
nested mapping objects into DotDicts, as well.
"""
val = self.__class__.build(val)
dict.__setitem__(self, key, val)
def __iter__(self):
"""Nodes are natively iterable, returning an iterator wrapping this instance.
This is slightly different from standard behavior: iterating a ``dict`` will
return its keys. Instead, we assume that the user is iterating an XML node
which they expect sometimes returns a list of nodes, and other times returns
a single instance of ``DotDict``. If the latter is true, we end up here.
So, we wrap this instance in an iterator, so that iterating on it will return
the ``DotDict`` itself, rather than its keys.
"""
return iter([self])
[docs] def update(self, *args, **kwargs):
"""Recursively builds values in any nested objects, such that any mapping
object in the nested structure is converted to a ``DotDict``.
- Each nested mapping object will be converted to ``DotDict``.
- Each non-string, non-dict iterable will have elements built, as well.
- All other objects in the data are left unchanged.
"""
for key, val in dict(*args, **kwargs).items():
self[key] = self.__class__.build(val)
[docs] @classmethod
def build(cls, obj):
"""Builds objects to work as recursive versions of this object.
- Mappings are converted to a DotDict object.
- For iterables, each element in the sequence is run through the build method recursively.
- All other objects are returned unchanged.
"""
if isinstance(obj, Mapping):
# Return a new DotDict object wrapping `obj`.
return cls(obj)
if not isinstance(obj, str) and isinstance(obj, Iterable):
# Build each item in the `obj` sequence,
# then construct a new sequence matching `obj`'s type.
# Must be careful not to pass strings here, even though they are iterable!
return obj.__class__(cls.build(x) for x in obj)
# In all other cases, return `obj` unchanged.
return obj