|
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) |
|
|
|
|
|
for key in left_keys - right_keys: |
|
merged[key] = left[key] |
|
|
|
|
|
for key in right_keys - left_keys: |
|
merged[key] = right[key] |
|
|
|
|
|
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)): |
|
merged[key] = merge(left_value, right_value) |
|
else: |
|
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 |