Completed
Push — master ( 8979a6...faf8a6 )
by Satoru
01:11
created

etree_to_container()   C

Complexity

Conditions 7

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
c 1
b 0
f 1
dl 0
loc 28
rs 5.5
1
#
2
# Copyright (C) 2011 - 2016 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.1.0
10
   Added XML dump support.
11
12
- Format to support: XML, e.g. http://www.w3.org/TR/xml11/
13
- Requirements: one of the followings
14
15
  - lxml2.etree if available
16
  - xml.etree.ElementTree in standard lib if python >= 2.5
17
  - elementtree.ElementTree (otherwise)
18
19
- Limitations:
20
21
  - '<prefix>attrs', '<prefix>text' and '<prefix>children' are used as special
22
    parameter to keep XML structure of original data. You have to cusomize
23
    <prefix> (default: '@') if any config parameters conflict with some of
24
    them.
25
26
  - Some data or structures of original XML file may be lost if make it backed
27
    to XML file; XML file - (anyconfig.load) -> config - (anyconfig.dump) ->
28
    XML file
29
30
  - XML specific features (namespace, etc.) may not be processed correctly.
31
32
- Special Options: None supported
33
"""
34
from __future__ import absolute_import
35
from io import BytesIO
36
37
import sys
38
39
import anyconfig.backend.base
40
import anyconfig.compat
41
import anyconfig.mdicts
42
43
try:
44
    # First, try lxml which is compatible with elementtree and looks faster a
45
    # lot. See also: http://getpython3.com/diveintopython3/xml.html
46
    from lxml2 import etree as ET
47
except ImportError:
48
    try:
49
        import xml.etree.ElementTree as ET
50
    except ImportError:
51
        import elementtree.ElementTree as ET
52
53
54
_PARAM_PREFIX = "@"
55
56
# It seems that ET.ElementTree.write() cannot process a parameter
57
# 'xml_declaration' in older python < 2.7:
58
_IS_OLDER_PYTHON = sys.version_info[0] < 3 and sys.version_info[1] < 7
59
60
61
def _gen_tags(pprefix=_PARAM_PREFIX):
62
    """
63
    Generate special prefixed tags.
64
65
    :param pprefix: Special parameter name prefix
66
    :return: A tuple of tags (attributes, text, children)
67
    """
68
    return (pprefix + x for x in ("attrs", "text", "children"))
69
70
71
def etree_to_container(root, cls, pprefix=_PARAM_PREFIX):
72
    """
73
    Convert XML ElementTree to a collection of container objects.
74
75
    :param root: etree root object or None
76
    :param cls: Container class
77
    :param pprefix: Special parameter name prefix
78
    """
79
    (attrs, text, children) = _gen_tags(pprefix)
80
    tree = cls()
81
    if root is None:
82
        return tree
83
84
    tree[root.tag] = cls()
85
86
    if root.attrib:
87
        tree[root.tag][attrs] = cls(root.attrib)
88
89
    if root.text and root.text.strip():
90
        tree[root.tag][text] = root.text.strip()
91
92
    if len(root):  # It has children.
93
        # Note: Configuration item cannot have both attributes and values
94
        # (list) at the same time in current implementation:
95
        tree[root.tag][children] = [etree_to_container(c, cls, pprefix)
96
                                    for c in root]
97
98
    return tree
99
100
101
def container_to_etree(obj, parent=None, pprefix=_PARAM_PREFIX):
102
    """
103
    Convert a dict-like object to XML ElementTree.
104
105
    :param obj: Container instance to convert to
106
    :param parent: XML ElementTree parent node object or None
107
    :param pprefix: Special parameter name prefix
108
    """
109
    if not anyconfig.mdicts.is_dict_like(obj):
110
        return  # All attributes and text should be set already.
111
112
    (attrs, text, children) = _gen_tags(pprefix)
113
    for key, val in anyconfig.compat.iteritems(obj):
114
        if key == attrs:
115
            for attr, aval in anyconfig.compat.iteritems(val):
116
                parent.set(attr, aval)
117
        elif key == text:
118
            parent.text = val
119
        elif key == children:
120
            for child in val:  # child should be a dict-like object.
121
                for ckey, cval in anyconfig.compat.iteritems(child):
122
                    celem = ET.Element(ckey)
123
                    container_to_etree(cval, celem, pprefix)
124
                    parent.append(celem)
125
        else:
126
            elem = ET.Element(key)
127
            container_to_etree(val, elem, pprefix)
128
            return ET.ElementTree(elem)
129
130
131
def etree_write(tree, stream):
132
    """
133
    Write XML ElementTree `root` content into `stream`.
134
135
    :param tree: XML ElementTree object
136
    :param stream: File or file-like object can write to
137
    """
138
    if _IS_OLDER_PYTHON:
139
        tree.write(stream, encoding='UTF-8')
140
    else:
141
        tree.write(stream, encoding='UTF-8', xml_declaration=True)
142
143
144
class Parser(anyconfig.backend.base.ToStreamDumper):
145
    """
146
    Parser for XML files.
147
    """
148
    _type = "xml"
149
    _extensions = ["xml"]
150
    _open_flags = ('rb', 'wb')
151
152
    def load_from_string(self, content, to_container, **kwargs):
153
        """
154
        Load config from XML snippet (a string `content`).
155
156
        :param content: XML snippet (a string)
157
        :param to_container: callble to make a container object
158
        :param kwargs: optional keyword parameters passed to
159
160
        :return: Dict-like object holding config parameters
161
        """
162
        root = ET.ElementTree(ET.fromstring(content)).getroot()
163
        return etree_to_container(root, to_container)
164
165
    def load_from_path(self, filepath, to_container, **kwargs):
166
        """
167
        :param filepath: XML file path
168
        :param to_container: callble to make a container object
169
        :param kwargs: optional keyword parameters to be sanitized
170
171
        :return: Dict-like object holding config parameters
172
        """
173
        root = ET.parse(filepath).getroot()
174
        return etree_to_container(root, to_container)
175
176
    def load_from_stream(self, stream, to_container, **kwargs):
177
        """
178
        :param stream: XML file or file-like object
179
        :param to_container: callble to make a container object
180
        :param kwargs: optional keyword parameters to be sanitized
181
182
        :return: Dict-like object holding config parameters
183
        """
184
        return self.load_from_path(stream, to_container, **kwargs)
185
186
    def dump_to_string(self, cnf, **kwargs):
187
        """
188
        :param cnf: Configuration data to dump
189
        :param kwargs: optional keyword parameters
190
191
        :return: string represents the configuration
192
        """
193
        tree = container_to_etree(cnf)
194
        buf = BytesIO()
195
        etree_write(tree, buf)
196
        return buf.getvalue()
197
198
    def dump_to_stream(self, cnf, stream, **kwargs):
199
        """
200
        :param cnf: Configuration data to dump
201
        :param stream: Config file or file like object write to
202
        :param kwargs: optional keyword parameters
203
        """
204
        tree = container_to_etree(cnf)
205
        etree_write(tree, stream)
206
207
# vim:sw=4:ts=4:et:
208