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