Completed
Push — master ( 548ddf...094624 )
by Satoru
01:05
created

flip()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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