1# encoding: utf-8 2 3""" 4Shared code for unit test data builders 5""" 6 7from __future__ import absolute_import, print_function, unicode_literals 8 9from docx.oxml import parse_xml 10from docx.oxml.ns import nsdecls 11 12 13class BaseBuilder(object): 14 """ 15 Provides common behavior for all data builders. 16 """ 17 def __init__(self): 18 self._empty = False 19 self._nsdecls = '' 20 self._text = '' 21 self._xmlattrs = [] 22 self._xmlattr_method_map = {} 23 for attr_name in self.__attrs__: 24 base_name = ( 25 attr_name.split(':')[1] if ':' in attr_name else attr_name 26 ) 27 method_name = 'with_%s' % base_name 28 self._xmlattr_method_map[method_name] = attr_name 29 self._child_bldrs = [] 30 31 def __getattr__(self, name): 32 """ 33 Intercept attribute access to generalize "with_{xmlattr_name}()" 34 methods. 35 """ 36 if name in self._xmlattr_method_map: 37 def with_xmlattr(value): 38 xmlattr_name = self._xmlattr_method_map[name] 39 self._set_xmlattr(xmlattr_name, value) 40 return self 41 return with_xmlattr 42 else: 43 tmpl = "'%s' object has no attribute '%s'" 44 raise AttributeError(tmpl % (self.__class__.__name__, name)) 45 46 def clear(self): 47 """ 48 Reset this builder back to initial state so it can be reused within 49 a single test. 50 """ 51 BaseBuilder.__init__(self) 52 return self 53 54 @property 55 def element(self): 56 """ 57 Element parsed from XML generated by builder in current state 58 """ 59 elm = parse_xml(self.xml()) 60 return elm 61 62 def with_child(self, child_bldr): 63 """ 64 Cause new child element specified by *child_bldr* to be appended to 65 the children of this element. 66 """ 67 self._child_bldrs.append(child_bldr) 68 return self 69 70 def with_text(self, text): 71 """ 72 Cause *text* to be placed between the start and end tags of this 73 element. Not robust enough for mixed elements, intended only for 74 elements having no child elements. 75 """ 76 self._text = text 77 return self 78 79 def with_nsdecls(self, *nspfxs): 80 """ 81 Cause the element to contain namespace declarations. By default, the 82 namespace prefixes defined in the Builder class are used. These can 83 be overridden by providing exlicit prefixes, e.g. 84 ``with_nsdecls('a', 'r')``. 85 """ 86 if not nspfxs: 87 nspfxs = self.__nspfxs__ 88 self._nsdecls = ' %s' % nsdecls(*nspfxs) 89 return self 90 91 def xml(self, indent=0): 92 """ 93 Return element XML based on attribute settings 94 """ 95 indent_str = ' ' * indent 96 if self._is_empty: 97 xml = '%s%s\n' % (indent_str, self._empty_element_tag) 98 else: 99 xml = '%s\n' % self._non_empty_element_xml(indent) 100 return xml 101 102 def xml_bytes(self, indent=0): 103 return self.xml(indent=indent).encode('utf-8') 104 105 @property 106 def _empty_element_tag(self): 107 return '<%s%s%s/>' % (self.__tag__, self._nsdecls, self._xmlattrs_str) 108 109 @property 110 def _end_tag(self): 111 return '</%s>' % self.__tag__ 112 113 @property 114 def _is_empty(self): 115 return len(self._child_bldrs) == 0 and len(self._text) == 0 116 117 def _non_empty_element_xml(self, indent): 118 indent_str = ' ' * indent 119 if self._text: 120 xml = ('%s%s%s%s' % 121 (indent_str, self._start_tag, self._text, self._end_tag)) 122 else: 123 xml = '%s%s\n' % (indent_str, self._start_tag) 124 for child_bldr in self._child_bldrs: 125 xml += child_bldr.xml(indent+2) 126 xml += '%s%s' % (indent_str, self._end_tag) 127 return xml 128 129 def _set_xmlattr(self, xmlattr_name, value): 130 xmlattr_str = ' %s="%s"' % (xmlattr_name, str(value)) 131 self._xmlattrs.append(xmlattr_str) 132 133 @property 134 def _start_tag(self): 135 return '<%s%s%s>' % (self.__tag__, self._nsdecls, self._xmlattrs_str) 136 137 @property 138 def _xmlattrs_str(self): 139 """ 140 Return all element attributes as a string, like ' foo="bar" x="1"'. 141 """ 142 return ''.join(self._xmlattrs) 143