Completed
Push — master ( bff2ab...5dc771 )
by Satoru
01:07
created

anyconfig.backend.container_to_etree()   F

Complexity

Conditions 10

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 10
dl 0
loc 29
rs 3.1304

How to fix   Complexity   

Complexity

Complex classes like anyconfig.backend.container_to_etree() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 etree_to_container(root, cls, pprefix=_PARAM_PREFIX):
62
    """
63
    Convert XML ElementTree to a collection of container objects.
64
65
    :param root: etree root object or None
66
    :param cls: Container class
67
    :param pprefix: Special parameter name prefix
68
    """
69
    (attrs, text, children) = [pprefix + x for x in ("attrs", "text",
70
                                                     "children")]
71
    tree = cls()
72
    if root is None:
73
        return tree
74
75
    tree[root.tag] = cls()
76
77
    if root.attrib:
78
        tree[root.tag][attrs] = cls(root.attrib)
79
80
    if root.text and root.text.strip():
81
        tree[root.tag][text] = root.text.strip()
82
83
    if len(root):  # It has children.
84
        # Note: Configuration item cannot have both attributes and values
85
        # (list) at the same time in current implementation:
86
        tree[root.tag][children] = [etree_to_container(c, cls, pprefix)
87
                                    for c in root]
88
89
    return tree
90
91
92
def container_to_etree(obj, parent=None, pprefix=_PARAM_PREFIX):
93
    """
94
    Convert a dict-like object to XML ElementTree.
95
96
    :param obj: Container instance to convert to
97
    :param parent: XML ElementTree parent node object or None
98
    :param pprefix: Special parameter name prefix
99
    """
100
    if not anyconfig.mdicts.is_dict_like(obj):
101
        return  # All attributes and text should be set already.
102
103
    (attrs, text, children) = [pprefix + x for x in ("attrs", "text",
104
                                                     "children")]
105
    for key, val in anyconfig.compat.iteritems(obj):
106
        if key == attrs:
107
            for attr, aval in anyconfig.compat.iteritems(val):
108
                parent.set(attr, aval)
109
        elif key == text:
110
            parent.text = val
111
        elif key == children:
112
            for child in val:  # child should be a dict-like object.
113
                for ckey, cval in anyconfig.compat.iteritems(child):
114
                    celem = ET.Element(ckey)
115
                    container_to_etree(cval, celem, pprefix)
116
                    parent.append(celem)
117
        else:
118
            elem = ET.Element(key)
119
            container_to_etree(val, elem, pprefix)
120
            return ET.ElementTree(elem)
121
122
123
def etree_write(tree, stream):
124
    """
125
    Write XML ElementTree `root` content into `stream`.
126
127
    :param tree: XML ElementTree object
128
    :param stream: File or file-like object can write to
129
    """
130
    if _IS_OLDER_PYTHON:
131
        tree.write(stream, encoding='UTF-8')
132
    else:
133
        tree.write(stream, encoding='UTF-8', xml_declaration=True)
134
135
136
class Parser(anyconfig.backend.base.ToStreamDumper):
137
    """
138
    Parser for XML files.
139
    """
140
    _type = "xml"
141
    _extensions = ["xml"]
142
    _open_flags = ('rb', 'wb')
143
144
    def load_from_string(self, content, to_container, **kwargs):
145
        """
146
        Load config from XML snippet (a string `content`).
147
148
        :param content: XML snippet (a string)
149
        :param to_container: callble to make a container object
150
        :param kwargs: optional keyword parameters passed to
151
152
        :return: Dict-like object holding config parameters
153
        """
154
        root = ET.ElementTree(ET.fromstring(content)).getroot()
155
        return etree_to_container(root, to_container)
156
157
    def load_from_path(self, filepath, to_container, **kwargs):
158
        """
159
        :param filepath: XML file path
160
        :param to_container: callble to make a container object
161
        :param kwargs: optional keyword parameters to be sanitized
162
163
        :return: Dict-like object holding config parameters
164
        """
165
        root = ET.parse(filepath).getroot()
166
        return etree_to_container(root, to_container)
167
168
    def load_from_stream(self, stream, to_container, **kwargs):
169
        """
170
        :param stream: XML file or file-like object
171
        :param to_container: callble to make a container object
172
        :param kwargs: optional keyword parameters to be sanitized
173
174
        :return: Dict-like object holding config parameters
175
        """
176
        return self.load_from_path(stream, to_container, **kwargs)
177
178
    def dump_to_string(self, cnf, **kwargs):
179
        """
180
        :param cnf: Configuration data to dump
181
        :param kwargs: optional keyword parameters
182
183
        :return: string represents the configuration
184
        """
185
        tree = container_to_etree(cnf)
186
        buf = BytesIO()
187
        etree_write(tree, buf)
188
        return buf.getvalue()
189
190
    def dump_to_stream(self, cnf, stream, **kwargs):
191
        """
192
        :param cnf: Configuration data to dump
193
        :param stream: Config file or file like object write to
194
        :param kwargs: optional keyword parameters
195
        """
196
        tree = container_to_etree(cnf)
197
        etree_write(tree, stream)
198
199
# vim:sw=4:ts=4:et:
200