"""
These containers keep track of the identity of the parent
object, and the attribute of the parent object that they correspond to.
"""
# ---------------------------
# Imports
# ---------------------------
from collections.abc import MutableSequence, MutableMapping
from .utils import check_type, encode_dict, debug
# ---------------------------
# Container Classes
# ---------------------------
# These are list and dict containers that
# (a) track the identity of their parents, and
# (b) track the parent's property where they are stored
# They attach these two to the elements requested through __getattr__
[docs]class ListContainer(MutableSequence):
"""
Wrapper around a list, to track the elements' parents.
**This class shouldn't be instantiated directly by users,
but by the elements that contain it**.
:param args: elements contained in the list--like object
:param oktypes: type or tuple of types that are allowed as items
:type oktypes: ``type`` | ``tuple``
:param parent: the parent element
:type parent: ``Element``
:param container: None, unless the element is not part of its .parent.content (this is the case for table headers for instance, which are not retrieved with table.content but with table.header)
:type container: ``str`` | None
"""
# Based on http://stackoverflow.com/a/3488283
# See also https://docs.python.org/3/library/collections.abc.html
__slots__ = ['list', 'oktypes', 'parent', 'location']
def __init__(self, *args, oktypes=object, parent=None):
self.oktypes = oktypes
self.parent = parent
self.location = None # Cannot be set through __init__
self.list = []
self.extend(args) # self.oktypes must be set first
def __contains__(self, item):
return item in self.list
def __len__(self):
return len(self.list)
def __getitem__(self, i):
if isinstance(i, int):
return attach(self.list[i], self.parent, self.location)
else:
newlist = self.list.__getitem__(i)
obj = ListContainer(*newlist,
oktypes=self.oktypes, parent=self.parent)
obj.location = self.location
return obj
def __delitem__(self, i):
del self.list[i]
def __setitem__(self, i, v):
if isinstance(i, slice):
v = (check_type(x, self.oktypes) for x in v)
else:
v = check_type(v, self.oktypes)
self.list[i] = v
[docs] def insert(self, i, v):
v = check_type(v, self.oktypes)
self.list.insert(i, v)
def __str__(self):
return self.__repr__()
def __repr__(self):
return 'ListContainer({})'.format(' '.join(repr(x) for x in self.list))
def to_json(self):
return [to_json_wrapper(item) for item in self.list]
[docs]class DictContainer(MutableMapping):
"""
Wrapper around a dict, to track the elements' parents.
**This class shouldn't be instantiated directly by users,
but by the elements that contain it**.
:param args: elements contained in the dict--like object
:param oktypes: type or tuple of types that are allowed as items
:type oktypes: ``type`` | ``tuple``
:param parent: the parent element
:type parent: ``Element``
"""
__slots__ = ['dict', 'oktypes', 'parent', 'location']
def __init__(self, *args, oktypes=object, parent=None, **kwargs):
self.oktypes = oktypes
self.parent = parent
self.location = None
self.dict = dict()
self.update(args) # Must be a sequence of tuples
self.update(kwargs) # Order of kwargs is not preserved
def __contains__(self, item):
return item in self.dict
def __len__(self):
return len(self.dict)
def __getitem__(self, k):
return attach(self.dict[k], self.parent, self.location)
def __delitem__(self, k):
del self.dict[k]
def __setitem__(self, k, v):
v = check_type(v, self.oktypes)
self.dict[k] = v
def __str__(self):
return self.__repr__()
def __repr__(self):
return 'DictContainer({})'.format(' '.join(repr(x) for x in self.dict))
def __iter__(self):
return self.dict.__iter__()
def to_json(self):
return {k: to_json_wrapper(v) for k, v in self.dict.items()}
# ---------------------------
# Functions
# ---------------------------
def attach(element, parent, location):
if not isinstance(element, (int, str, bool)):
element.parent = parent
element.location = location
else:
debug(f'Warning: element "{type(element)}" has no parent')
return element
def to_json_wrapper(e):
if isinstance(e, str):
return e
elif isinstance(e, bool):
return encode_dict('MetaBool', e)
else:
return e.to_json()