Completed
Push — master ( 22be07...0a8a0f )
by Satoru
01:01
created

_elem_from_descendants()   A

Complexity

Conditions 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
1
#
2
# Copyright (C) 2011 - 2017 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
# Some XML modules may be missing and Base.{load,dumps}_impl are not overriden:
6
# pylint: disable=import-error
7
"""XML files parser backend, should be available always.
8
9
- Format to support: XML, e.g. http://www.w3.org/TR/xml11/
10
- Requirements: one of the followings
11
12
  - lxml2.etree if available
13
  - xml.etree.ElementTree in standard lib if python >= 2.5
14
  - elementtree.ElementTree (otherwise)
15
16
- Development Status: 3 - Alpha
17
- Limitations:
18
19
  - '<prefix>attrs', '<prefix>text' and '<prefix>children' are used as special
20
    parameter to keep XML structure of original data. You have to cusomize
21
    <prefix> (default: '@') if any config parameters conflict with some of
22
    them.
23
24
  - Some data or structures of original XML file may be lost if make it backed
25
    to XML file; XML file - (anyconfig.load) -> config - (anyconfig.dump) ->
26
    XML file
27
28
  - XML specific features (namespace, etc.) may not be processed correctly.
29
30
- Special Options:
31
  - pprefix: Specify parameter prefix for attributes, text and children nodes.
32
33
History:
34
35
.. versionchanged:: 0.7.99
36
37
   - Try to make a nested dict w/o extra dict having keys of attrs, text and
38
     children from XML string/file as much as possible.
39
   - Support namespaces partially.
40
41
.. versionchanged:: 0.1.0
42
43
   - Added XML dump support.
44
"""
45
from __future__ import absolute_import
46
from io import BytesIO
47
48
import re
49
try:
50
    # First, try lxml which is compatible with elementtree and looks faster a
51
    # lot. See also: http://getpython3.com/diveintopython3/xml.html
52
    from lxml2 import etree as ET
53
except ImportError:
54
    try:
55
        import xml.etree.cElementTree as ET
56
    except ImportError:
57
        import xml.etree.ElementTree as ET
58
    except ImportError:
59
        import elementtree.ElementTree as ET
60
61
import anyconfig.backend.base
62
import anyconfig.compat
63
import anyconfig.mdicts
64
import anyconfig.utils
65
66
67
_PREFIX = "@"
68
69
_ET_NS_RE = re.compile(r"^{(\S+)}(\S+)$")
70
71
72
def _iterparse(xmlfile):
73
    """
74
    Avoid bug in python 3.{2,3}. See http://bugs.python.org/issue9257.
75
76
    :param xmlfile: XML file or file-like object
77
    """
78
    try:
79
        return ET.iterparse(xmlfile, events=("start-ns", ))
80
    except TypeError:
81
        return ET.iterparse(xmlfile, events=(b"start-ns", ))
82
83
84
def flip(tpl):
85
    """
86
    >>> flip((1, 2))
87
    (2, 1)
88
    """
89
    return (tpl[1], tpl[0])
90
91
92
def _namespaces_from_file(xmlfile):
93
    """
94
    :param xmlfile: XML file or file-like object
95
    :return: {namespace_uri: namespace_prefix} or {}
96
    """
97
    return dict(flip(t) for _, t in _iterparse(xmlfile))
98
99
100
def _gen_tags(pprefix=_PREFIX):
101
    """
102
    Generate special prefixed tags.
103
104
    :param pprefix: Special parameter name prefix
105
    :return: A tuple of prefixed (attributes, text, children)
106
    """
107
    return tuple(pprefix + x for x in ("attrs", "text", "children"))
108
109
110
def _tweak_ns(tag, nspaces):
111
    """
112
    :param tag: XML tag element
113
    :param nspaces: A namespaces dict, {uri: prefix}
114
115
    >>> _tweak_ns("a", {})
116
    'a'
117
    >>> _tweak_ns("a", {"http://example.com/ns/val/": "val"})
118
    'a'
119
    >>> _tweak_ns("{http://example.com/ns/val/}a",
120
    ...           {"http://example.com/ns/val/": "val"})
121
    'val:a'
122
    """
123
    if nspaces:
124
        matched = _ET_NS_RE.match(tag)
125
        if matched:
126
            (uri, tag) = matched.groups()
127
            prefix = nspaces.get(uri, False)
128
            if prefix:
129
                return "%s:%s" % (prefix, tag)
130
131
    return tag
132
133
134
def elem_to_container(elem, to_container, nspaces, tags=False):
135
    """
136
    Convert XML ElementTree Element to a collection of container objects.
137
138
    :param elem: etree elem object or None
139
    :param to_container: callble to make a container object
140
    :param nspaces: A namespaces dict, {uri: prefix}
141
    :param tags: (attrs, text, children) parameter names
142
    """
143
    tree = to_container()
144
    if elem is None:
145
        return tree
146
147
    subtree = tree[_tweak_ns(elem.tag, nspaces)] = to_container()
148
    (attrs, text, children) = tags if tags else _gen_tags()
149
    _num_of_children = len(elem)
150
151
    if elem.attrib:
152
        subtree[attrs] = to_container(elem.attrib)
153
154
    if elem.text:
155
        elem.text = elem.text.strip()
156
        if elem.text:
157
            if not _num_of_children and not elem.attrib:
158
                # .. note:: Treat as special case for later convenience.
159
                tree[elem.tag] = elem.text
160
            else:
161
                subtree[text] = elem.text
162
163
    if _num_of_children:
164
        # Note: Configuration item cannot have both attributes and values
165
        # (list) at the same time in current implementation:
166
        args = (to_container, nspaces, tags)
167
        if _num_of_children == 1:  # .. note:: Another special case.
168
            tree[elem.tag] = [elem_to_container(c, *args) for c in elem][0]
169
        else:
170
            subtree[children] = [elem_to_container(c, *args) for c in elem]
171
172
    return tree
173
174
175
def root_to_container(root, to_container, nspaces, pprefix=_PREFIX):
176
    """
177
    Convert XML ElementTree Root Element to a collection of container objects.
178
179
    :param root: etree root object or None
180
    :param to_container: callble to make a container object
181
    :param nspaces: A namespaces dict, {uri: prefix}
182
    :param pprefix: Special parameter name prefix
183
    """
184
    tree = to_container()
185
    if root is None:
186
        return tree
187
188
    if nspaces is None:
189
        nspaces = dict()
190
191
    if nspaces:
192
        for uri, prefix in nspaces.items():
193
            root.attrib["xmlns:" + prefix if prefix else "xmlns"] = uri
194
195
    return elem_to_container(root, to_container, nspaces, _gen_tags(pprefix))
196
197
198
def _elem_from_descendants(children, pprefix=_PREFIX):
199
    """
200
    :param children: A list of child dict objects
201
    :param pprefix: Special parameter name prefix
202
    """
203
    for child in children:  # child should be a dict-like object.
204
        for ckey, cval in anyconfig.compat.iteritems(child):
205
            celem = ET.Element(ckey)
206
            container_to_etree(cval, parent=celem, pprefix=pprefix)
207
            yield celem
208
209
210
def _make_etree(key, val, parent=None, pprefix=_PREFIX):
211
    """
212
    :param key: Key of current child (dict{,-like} object)
213
    :param val: Value of current child (dict{,-like} object)
214
    :param parent: XML ElementTree parent node object or None
215
    :param pprefix: Special parameter name prefix
216
    """
217
    elem = ET.Element(key)
218
    container_to_etree(val, parent=elem, pprefix=pprefix)
219
    if parent is None:  # 'elem' is the top level etree.
220
        return ET.ElementTree(elem)
221
    else:
222
        parent.append(elem)
223
        return ET.ElementTree(parent)
224
225
226
def container_to_etree(obj, parent=None, pprefix=_PREFIX):
227
    """
228
    Convert a dict-like object to XML ElementTree.
229
230
    :param obj: Container instance to convert to
231
    :param parent: XML ElementTree parent node object or None
232
    :param pprefix: Special parameter name prefix
233
    """
234
    if not anyconfig.mdicts.is_dict_like(obj):
235
        if parent is not None and obj:
236
            parent.text = obj  # Parent is a leaf text node.
237
        return  # All attributes and text should be set already.
238
239
    (attrs, text, children) = _gen_tags(pprefix)
240
    for key, val in anyconfig.compat.iteritems(obj):
241
        if key == attrs:
242
            for attr, aval in anyconfig.compat.iteritems(val):
243
                parent.set(attr, aval)
244
        elif key == text:
245
            parent.text = val
246
        elif key == children:
247
            for celem in _elem_from_descendants(val, pprefix=pprefix):
248
                parent.append(celem)
249
        else:
250
            return _make_etree(key, val, parent=parent, pprefix=pprefix)
251
252
253
def etree_write(tree, stream):
254
    """
255
    Write XML ElementTree `root` content into `stream`.
256
257
    .. note:
258
       It seems that ET.ElementTree.write() cannot process a parameter
259
       'xml_declaration' in python 2.6.
260
261
    :param tree: XML ElementTree object
262
    :param stream: File or file-like object can write to
263
    """
264
    if anyconfig.compat.IS_PYTHON_2_6:
265
        tree.write(stream, encoding='UTF-8')
266
    else:
267
        tree.write(stream, encoding='UTF-8', xml_declaration=True)
268
269
270
class Parser(anyconfig.backend.base.ToStreamDumper):
271
    """
272
    Parser for XML files.
273
    """
274
    _type = "xml"
275
    _extensions = ["xml"]
276
    _open_flags = ('rb', 'wb')
277
    _load_opts = _dump_opts = ["pprefix"]
278
279
    def load_from_string(self, content, to_container, **kwargs):
280
        """
281
        Load config from XML snippet (a string `content`).
282
283
        :param content:
284
            XML snippet string of str (python 2) or bytes (python 3) type
285
        :param to_container: callble to make a container object
286
        :param kwargs: optional keyword parameters passed to
287
288
        :return: Dict-like object holding config parameters
289
        """
290
        root = ET.fromstring(content)
291
        if anyconfig.compat.IS_PYTHON_3:
292
            stream = BytesIO(content)
293
        else:
294
            stream = anyconfig.compat.StringIO(content)
295
        nspaces = _namespaces_from_file(stream)
296
        return root_to_container(root, to_container, nspaces, **kwargs)
297
298
    def load_from_path(self, filepath, to_container, **kwargs):
299
        """
300
        :param filepath: XML file path
301
        :param to_container: callble to make a container object
302
        :param kwargs: optional keyword parameters to be sanitized
303
304
        :return: Dict-like object holding config parameters
305
        """
306
        root = ET.parse(filepath).getroot()
307
        nspaces = _namespaces_from_file(filepath)
308
        return root_to_container(root, to_container, nspaces, **kwargs)
309
310
    def load_from_stream(self, stream, to_container, **kwargs):
311
        """
312
        :param stream: XML file or file-like object
313
        :param to_container: callble to make a container object
314
        :param kwargs: optional keyword parameters to be sanitized
315
316
        :return: Dict-like object holding config parameters
317
        """
318
        root = ET.parse(stream).getroot()
319
        path = anyconfig.utils.get_path_from_stream(stream)
320
        nspaces = _namespaces_from_file(path)
321
        return root_to_container(root, to_container, nspaces, **kwargs)
322
323
    def dump_to_string(self, cnf, **kwargs):
324
        """
325
        :param cnf: Configuration data to dump
326
        :param kwargs: optional keyword parameters
327
328
        :return: string represents the configuration
329
        """
330
        tree = container_to_etree(cnf, **kwargs)
331
        buf = BytesIO()
332
        etree_write(tree, buf)
333
        return buf.getvalue()
334
335
    def dump_to_stream(self, cnf, stream, **kwargs):
336
        """
337
        :param cnf: Configuration data to dump
338
        :param stream: Config file or file like object write to
339
        :param kwargs: optional keyword parameters
340
        """
341
        tree = container_to_etree(cnf, **kwargs)
342
        etree_write(tree, stream)
343
344
# vim:sw=4:ts=4:et:
345