tiny-random-janus / attrdict_config.py
katuni4ka's picture
Upload 18 files
f0b37fb verified
from abc import ABCMeta, abstractmethod
try:
from collections import Mapping, MutableMapping, Sequence
except ImportError:
from collections.abc import Mapping, MutableMapping, Sequence
import re
import six
__all__ = ['Attr', 'MutableAttr']
def merge(left, right):
"""
Merge two mappings objects together, combining overlapping Mappings,
and favoring right-values
left: The left Mapping object.
right: The right (favored) Mapping object.
NOTE: This is not commutative (merge(a,b) != merge(b,a)).
"""
merged = {}
left_keys = frozenset(left)
right_keys = frozenset(right)
# Items only in the left Mapping
for key in left_keys - right_keys:
merged[key] = left[key]
# Items only in the right Mapping
for key in right_keys - left_keys:
merged[key] = right[key]
# in both
for key in left_keys & right_keys:
left_value = left[key]
right_value = right[key]
if (isinstance(left_value, Mapping) and
isinstance(right_value, Mapping)): # recursive merge
merged[key] = merge(left_value, right_value)
else: # overwrite with right value
merged[key] = right_value
return merged
@six.add_metaclass(ABCMeta)
class Attr(Mapping):
"""
A mixin class for a mapping that allows for attribute-style access
of values.
A key may be used as an attribute if:
* It is a string
* It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute)
* The key doesn't overlap with any class attributes (for Attr,
those would be 'get', 'items', 'keys', 'values', 'mro', and
'register').
If a values which is accessed as an attribute is a Sequence-type
(and is not a string/bytes), it will be converted to a
_sequence_type with any mappings within it converted to Attrs.
NOTE: This means that if _sequence_type is not None, then a
sequence accessed as an attribute will be a different object
than if accessed as an attribute than if it is accessed as an
item.
"""
@abstractmethod
def _configuration(self):
"""
All required state for building a new instance with the same
settings as the current object.
"""
@classmethod
def _constructor(cls, mapping, configuration):
"""
A standardized constructor used internally by Attr.
mapping: A mapping of key-value pairs. It is HIGHLY recommended
that you use this as the internal key-value pair mapping, as
that will allow nested assignment (e.g., attr.foo.bar = baz)
configuration: The return value of Attr._configuration
"""
raise NotImplementedError("You need to implement this")
def __call__(self, key):
"""
Dynamically access a key-value pair.
key: A key associated with a value in the mapping.
This differs from __getitem__, because it returns a new instance
of an Attr (if the value is a Mapping object).
"""
if key not in self:
raise AttributeError(
"'{cls} instance has no attribute '{name}'".format(
cls=self.__class__.__name__, name=key
)
)
return self._build(self[key])
def __getattr__(self, key):
"""
Access an item as an attribute.
"""
if key not in self or not self._valid_name(key):
raise AttributeError(
"'{cls}' instance has no attribute '{name}'".format(
cls=self.__class__.__name__, name=key
)
)
return self._build(self[key])
def __add__(self, other):
"""
Add a mapping to this Attr, creating a new, merged Attr.
other: A mapping.
NOTE: Addition is not commutative. a + b != b + a.
"""
if not isinstance(other, Mapping):
return NotImplemented
return self._constructor(merge(self, other), self._configuration())
def __radd__(self, other):
"""
Add this Attr to a mapping, creating a new, merged Attr.
other: A mapping.
NOTE: Addition is not commutative. a + b != b + a.
"""
if not isinstance(other, Mapping):
return NotImplemented
return self._constructor(merge(other, self), self._configuration())
def _build(self, obj):
"""
Conditionally convert an object to allow for recursive mapping
access.
obj: An object that was a key-value pair in the mapping. If obj
is a mapping, self._constructor(obj, self._configuration())
will be called. If obj is a non-string/bytes sequence, and
self._sequence_type is not None, the obj will be converted
to type _sequence_type and build will be called on its
elements.
"""
if isinstance(obj, Mapping):
obj = self._constructor(obj, self._configuration())
elif (isinstance(obj, Sequence) and
not isinstance(obj, (six.string_types, six.binary_type))):
sequence_type = getattr(self, '_sequence_type', None)
if sequence_type:
obj = sequence_type(self._build(element) for element in obj)
return obj
@classmethod
def _valid_name(cls, key):
"""
Check whether a key is a valid attribute name.
A key may be used as an attribute if:
* It is a string
* It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute)
* The key doesn't overlap with any class attributes (for Attr,
those would be 'get', 'items', 'keys', 'values', 'mro', and
'register').
"""
return (
isinstance(key, six.string_types) and
re.match('^[A-Za-z][A-Za-z0-9_]*$', key) and
not hasattr(cls, key)
)
@six.add_metaclass(ABCMeta)
class MutableAttr(Attr, MutableMapping):
"""
A mixin class for a mapping that allows for attribute-style access
of values.
"""
def _setattr(self, key, value):
"""
Add an attribute to the object, without attempting to add it as
a key to the mapping.
"""
super(MutableAttr, self).__setattr__(key, value)
def __setattr__(self, key, value):
"""
Add an attribute.
key: The name of the attribute
value: The attributes contents
"""
if self._valid_name(key):
self[key] = value
elif getattr(self, '_allow_invalid_attributes', True):
super(MutableAttr, self).__setattr__(key, value)
else:
raise TypeError(
"'{cls}' does not allow attribute creation.".format(
cls=self.__class__.__name__
)
)
def _delattr(self, key):
"""
Delete an attribute from the object, without attempting to
remove it from the mapping.
"""
super(MutableAttr, self).__delattr__(key)
def __delattr__(self, key, force=False):
"""
Delete an attribute.
key: The name of the attribute
"""
if self._valid_name(key):
del self[key]
elif getattr(self, '_allow_invalid_attributes', True):
super(MutableAttr, self).__delattr__(key)
else:
raise TypeError(
"'{cls}' does not allow attribute deletion.".format(
cls=self.__class__.__name__
)
)
class AttrDict(dict, MutableAttr):
"""
A dict that implements MutableAttr.
"""
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self._setattr('_sequence_type', tuple)
self._setattr('_allow_invalid_attributes', False)
def _configuration(self):
"""
The configuration for an attrmap instance.
"""
return self._sequence_type
def __getstate__(self):
"""
Serialize the object.
"""
return (
self.copy(),
self._sequence_type,
self._allow_invalid_attributes
)
def __setstate__(self, state):
"""
Deserialize the object.
"""
mapping, sequence_type, allow_invalid_attributes = state
self.update(mapping)
self._setattr('_sequence_type', sequence_type)
self._setattr('_allow_invalid_attributes', allow_invalid_attributes)
def __repr__(self):
return six.u('AttrDict({contents})').format(
contents=super(AttrDict, self).__repr__()
)
@classmethod
def _constructor(cls, mapping, configuration):
"""
A standardized constructor.
"""
attr = cls(mapping)
attr._setattr('_sequence_type', configuration)
return attr