"""Shared code for unit test data builders.""" from docx.oxml.ns import nsdecls from docx.oxml.parser import parse_xml class BaseBuilder: """ Provides common behavior for all data builders. """ def __init__(self): self._empty = False self._nsdecls = "" self._text = "" self._xmlattrs = [] self._xmlattr_method_map = {} for attr_name in self.__attrs__: base_name = attr_name.split(":")[1] if ":" in attr_name else attr_name method_name = "with_%s" % base_name self._xmlattr_method_map[method_name] = attr_name self._child_bldrs = [] def __getattr__(self, name): """ Intercept attribute access to generalize "with_{xmlattr_name}()" methods. """ if name in self._xmlattr_method_map: def with_xmlattr(value): xmlattr_name = self._xmlattr_method_map[name] self._set_xmlattr(xmlattr_name, value) return self return with_xmlattr else: tmpl = "'%s' object has no attribute '%s'" raise AttributeError(tmpl % (self.__class__.__name__, name)) def clear(self): """ Reset this builder back to initial state so it can be reused within a single test. """ BaseBuilder.__init__(self) return self @property def element(self): """ Element parsed from XML generated by builder in current state """ elm = parse_xml(self.xml()) return elm def with_child(self, child_bldr): """ Cause new child element specified by `child_bldr` to be appended to the children of this element. """ self._child_bldrs.append(child_bldr) return self def with_text(self, text): """ Cause `text` to be placed between the start and end tags of this element. Not robust enough for mixed elements, intended only for elements having no child elements. """ self._text = text return self def with_nsdecls(self, *nspfxs): """ Cause the element to contain namespace declarations. By default, the namespace prefixes defined in the Builder class are used. These can be overridden by providing exlicit prefixes, e.g. ``with_nsdecls('a', 'r')``. """ if not nspfxs: nspfxs = self.__nspfxs__ self._nsdecls = " %s" % nsdecls(*nspfxs) return self def xml(self, indent=0): """ Return element XML based on attribute settings """ indent_str = " " * indent if self._is_empty: xml = "%s%s\n" % (indent_str, self._empty_element_tag) else: xml = "%s\n" % self._non_empty_element_xml(indent) return xml def xml_bytes(self, indent=0): return self.xml(indent=indent).encode("utf-8") @property def _empty_element_tag(self): return "<%s%s%s/>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @property def _end_tag(self): return "" % self.__tag__ @property def _is_empty(self): return len(self._child_bldrs) == 0 and len(self._text) == 0 def _non_empty_element_xml(self, indent): indent_str = " " * indent if self._text: xml = "%s%s%s%s" % (indent_str, self._start_tag, self._text, self._end_tag) else: xml = "%s%s\n" % (indent_str, self._start_tag) for child_bldr in self._child_bldrs: xml += child_bldr.xml(indent + 2) xml += "%s%s" % (indent_str, self._end_tag) return xml def _set_xmlattr(self, xmlattr_name, value): xmlattr_str = ' %s="%s"' % (xmlattr_name, str(value)) self._xmlattrs.append(xmlattr_str) @property def _start_tag(self): return "<%s%s%s>" % (self.__tag__, self._nsdecls, self._xmlattrs_str) @property def _xmlattrs_str(self): """ Return all element attributes as a string, like ' foo="bar" x="1"'. """ return "".join(self._xmlattrs)