241 lines
7.2 KiB
Python
241 lines
7.2 KiB
Python
# Copyright (c) 2010-2024 openpyxl
|
|
|
|
from copy import copy
|
|
from keyword import kwlist
|
|
KEYWORDS = frozenset(kwlist)
|
|
|
|
from . import Descriptor
|
|
from . import MetaSerialisable
|
|
from .sequence import (
|
|
Sequence,
|
|
NestedSequence,
|
|
MultiSequencePart,
|
|
)
|
|
from .namespace import namespaced
|
|
|
|
from openpyxl.compat import safe_string
|
|
from openpyxl.xml.functions import (
|
|
Element,
|
|
localname,
|
|
)
|
|
|
|
seq_types = (list, tuple)
|
|
|
|
class Serialisable(metaclass=MetaSerialisable):
|
|
"""
|
|
Objects can serialise to XML their attributes and child objects.
|
|
The following class attributes are created by the metaclass at runtime:
|
|
__attrs__ = attributes
|
|
__nested__ = single-valued child treated as an attribute
|
|
__elements__ = child elements
|
|
"""
|
|
|
|
__attrs__ = None
|
|
__nested__ = None
|
|
__elements__ = None
|
|
__namespaced__ = None
|
|
|
|
idx_base = 0
|
|
|
|
@property
|
|
def tagname(self):
|
|
raise(NotImplementedError)
|
|
|
|
namespace = None
|
|
|
|
@classmethod
|
|
def from_tree(cls, node):
|
|
"""
|
|
Create object from XML
|
|
"""
|
|
# strip known namespaces from attributes
|
|
attrib = dict(node.attrib)
|
|
for key, ns in cls.__namespaced__:
|
|
if ns in attrib:
|
|
attrib[key] = attrib[ns]
|
|
del attrib[ns]
|
|
|
|
# strip attributes with unknown namespaces
|
|
for key in list(attrib):
|
|
if key.startswith('{'):
|
|
del attrib[key]
|
|
elif key in KEYWORDS:
|
|
attrib["_" + key] = attrib[key]
|
|
del attrib[key]
|
|
elif "-" in key:
|
|
n = key.replace("-", "_")
|
|
attrib[n] = attrib[key]
|
|
del attrib[key]
|
|
|
|
if node.text and "attr_text" in cls.__attrs__:
|
|
attrib["attr_text"] = node.text
|
|
|
|
for el in node:
|
|
tag = localname(el)
|
|
if tag in KEYWORDS:
|
|
tag = "_" + tag
|
|
desc = getattr(cls, tag, None)
|
|
if desc is None or isinstance(desc, property):
|
|
continue
|
|
|
|
if hasattr(desc, 'from_tree'):
|
|
#descriptor manages conversion
|
|
obj = desc.from_tree(el)
|
|
else:
|
|
if hasattr(desc.expected_type, "from_tree"):
|
|
#complex type
|
|
obj = desc.expected_type.from_tree(el)
|
|
else:
|
|
#primitive
|
|
obj = el.text
|
|
|
|
if isinstance(desc, NestedSequence):
|
|
attrib[tag] = obj
|
|
elif isinstance(desc, Sequence):
|
|
attrib.setdefault(tag, [])
|
|
attrib[tag].append(obj)
|
|
elif isinstance(desc, MultiSequencePart):
|
|
attrib.setdefault(desc.store, [])
|
|
attrib[desc.store].append(obj)
|
|
else:
|
|
attrib[tag] = obj
|
|
|
|
return cls(**attrib)
|
|
|
|
|
|
def to_tree(self, tagname=None, idx=None, namespace=None):
|
|
|
|
if tagname is None:
|
|
tagname = self.tagname
|
|
|
|
# keywords have to be masked
|
|
if tagname.startswith("_"):
|
|
tagname = tagname[1:]
|
|
|
|
tagname = namespaced(self, tagname, namespace)
|
|
namespace = getattr(self, "namespace", namespace)
|
|
|
|
attrs = dict(self)
|
|
for key, ns in self.__namespaced__:
|
|
if key in attrs:
|
|
attrs[ns] = attrs[key]
|
|
del attrs[key]
|
|
|
|
el = Element(tagname, attrs)
|
|
if "attr_text" in self.__attrs__:
|
|
el.text = safe_string(getattr(self, "attr_text"))
|
|
|
|
for child_tag in self.__elements__:
|
|
desc = getattr(self.__class__, child_tag, None)
|
|
obj = getattr(self, child_tag)
|
|
if hasattr(desc, "namespace") and hasattr(obj, 'namespace'):
|
|
obj.namespace = desc.namespace
|
|
|
|
if isinstance(obj, seq_types):
|
|
if isinstance(desc, NestedSequence):
|
|
# wrap sequence in container
|
|
if not obj:
|
|
continue
|
|
nodes = [desc.to_tree(child_tag, obj, namespace)]
|
|
elif isinstance(desc, Sequence):
|
|
# sequence
|
|
desc.idx_base = self.idx_base
|
|
nodes = (desc.to_tree(child_tag, obj, namespace))
|
|
else: # property
|
|
nodes = (v.to_tree(child_tag, namespace) for v in obj)
|
|
for node in nodes:
|
|
el.append(node)
|
|
else:
|
|
if child_tag in self.__nested__:
|
|
node = desc.to_tree(child_tag, obj, namespace)
|
|
elif obj is None:
|
|
continue
|
|
else:
|
|
node = obj.to_tree(child_tag)
|
|
if node is not None:
|
|
el.append(node)
|
|
return el
|
|
|
|
|
|
def __iter__(self):
|
|
for attr in self.__attrs__:
|
|
value = getattr(self, attr)
|
|
if attr.startswith("_"):
|
|
attr = attr[1:]
|
|
elif attr != "attr_text" and "_" in attr:
|
|
desc = getattr(self.__class__, attr)
|
|
if getattr(desc, "hyphenated", False):
|
|
attr = attr.replace("_", "-")
|
|
if attr != "attr_text" and value is not None:
|
|
yield attr, safe_string(value)
|
|
|
|
|
|
def __eq__(self, other):
|
|
if not self.__class__ == other.__class__:
|
|
return False
|
|
elif not dict(self) == dict(other):
|
|
return False
|
|
for el in self.__elements__:
|
|
if getattr(self, el) != getattr(other, el):
|
|
return False
|
|
return True
|
|
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
|
|
def __repr__(self):
|
|
s = u"<{0}.{1} object>\nParameters:".format(
|
|
self.__module__,
|
|
self.__class__.__name__
|
|
)
|
|
args = []
|
|
for k in self.__attrs__ + self.__elements__:
|
|
v = getattr(self, k)
|
|
if isinstance(v, Descriptor):
|
|
v = None
|
|
args.append(u"{0}={1}".format(k, repr(v)))
|
|
args = u", ".join(args)
|
|
|
|
return u"\n".join([s, args])
|
|
|
|
|
|
def __hash__(self):
|
|
fields = []
|
|
for attr in self.__attrs__ + self.__elements__:
|
|
val = getattr(self, attr)
|
|
if isinstance(val, list):
|
|
val = tuple(val)
|
|
fields.append(val)
|
|
|
|
return hash(tuple(fields))
|
|
|
|
|
|
def __add__(self, other):
|
|
if type(self) != type(other):
|
|
raise TypeError("Cannot combine instances of different types")
|
|
vals = {}
|
|
for attr in self.__attrs__:
|
|
vals[attr] = getattr(self, attr) or getattr(other, attr)
|
|
for el in self.__elements__:
|
|
a = getattr(self, el)
|
|
b = getattr(other, el)
|
|
if a and b:
|
|
vals[el] = a + b
|
|
else:
|
|
vals[el] = a or b
|
|
return self.__class__(**vals)
|
|
|
|
|
|
def __copy__(self):
|
|
# serialise to xml and back to avoid shallow copies
|
|
xml = self.to_tree(tagname="dummy")
|
|
cp = self.__class__.from_tree(xml)
|
|
# copy any non-persisted attributed
|
|
for k in self.__dict__:
|
|
if k not in self.__attrs__ + self.__elements__:
|
|
v = copy(getattr(self, k))
|
|
setattr(cp, k, v)
|
|
return cp
|