_customized_loader()   C
last analyzed

Complexity

Conditions 8

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
dl 0
loc 47
rs 6.8678
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A construct_ustr() 0 3 1
A construct_mapping() 0 24 4
1
#
2
# Copyright (C) 2011 - 2018 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
# type() is used to exactly match check instead of isinstance here.
6
# pylint: disable=unidiomatic-typecheck
7
r"""YAML backend:
8
9
- Format to support: YAML, http://yaml.org
10
- Requirements:
11
12
  - ruamel.yaml, https://bitbucket.org/ruamel/yaml
13
  - or PyYAML (yaml), http://pyyaml.org
14
15
- Development Status :: 5 - Production/Stable
16
- Limitations:
17
18
  - Resuls is not ordered even if 'ac_ordered' or 'ac_dict' was given.
19
20
- Special options:
21
22
  - All keyword options of yaml.safe_load, yaml.load, yaml.safe_dump and
23
    yaml.dump should work.
24
25
  - Use 'ac_safe' boolean keyword option if you prefer to call yaml.safe_load
26
    and yaml.safe_dump instead of yaml.load and yaml.dump. Please note that
27
    this option conflicts with 'ac_dict' option and these options cannot be
28
    used at the same time.
29
30
  - See also: http://pyyaml.org/wiki/PyYAMLDocumentation
31
32
Changelog:
33
34
.. versionchanged:: 0.9.6
35
36
   - Add support of loading primitives other than mapping objects.
37
38
.. versionchanged:: 0.9.3
39
40
   - Try ruamel.yaml instead of yaml (PyYAML) if it's available.
41
42
.. versionchanged:: 0.3
43
44
   - Changed special keyword option 'ac_safe' from 'safe' to avoid
45
     possibility of option conflicts in the future.
46
"""
47
from __future__ import absolute_import
48
49
try:
50
    import warnings
51
    import ruamel.yaml as yaml
52
    from ruamel.yaml import Loader, Dumper
53
    warnings.simplefilter('ignore', yaml.error.UnsafeLoaderWarning)
54
except ImportError:
55
    import yaml
56
    try:
57
        from yaml import CSafeLoader as Loader, CDumper as Dumper
58
    except ImportError:
59
        from yaml import SafeLoader as Loader, Dumper
60
61
import anyconfig.backend.base
62
import anyconfig.compat
63
import anyconfig.utils
64
65
66
_MAPPING_TAG = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
67
68
69
def _filter_from_options(key, options):
70
    """
71
    :param key: Key str in options
72
    :param options: Mapping object
73
    :return:
74
        New mapping object from `options` in which the item with `key` filtered
75
76
    >>> _filter_from_options('a', dict(a=1, b=2))
77
    {'b': 2}
78
    """
79
    return anyconfig.utils.filter_options([k for k in options.keys()
80
                                           if k != key], options)
81
82
83
def _customized_loader(container, loader=Loader, mapping_tag=_MAPPING_TAG):
84
    """
85
    Create or update loader with making given callble `container` to make
86
    mapping objects such as dict and OrderedDict, used to construct python
87
    object from yaml mapping node internally.
88
89
    :param container: Set container used internally
90
    """
91
    def construct_mapping(loader, node, deep=False):
92
        """Construct python object from yaml mapping node, based on
93
        :meth:`yaml.BaseConstructor.construct_mapping` in PyYAML (MIT).
94
        """
95
        loader.flatten_mapping(node)
96
        if not isinstance(node, yaml.MappingNode):
97
            msg = "expected a mapping node, but found %s" % node.id
98
            raise yaml.constructor.ConstructorError(None, None, msg,
99
                                                    node.start_mark)
100
        mapping = container()
101
        for key_node, value_node in node.value:
102
            key = loader.construct_object(key_node, deep=deep)
103
            try:
104
                hash(key)
105
            except TypeError as exc:
106
                eargs = ("while constructing a mapping",
107
                         node.start_mark,
108
                         "found unacceptable key (%s)" % exc,
109
                         key_node.start_mark)
110
                raise yaml.constructor.ConstructorError(*eargs)
111
            value = loader.construct_object(value_node, deep=deep)
112
            mapping[key] = value
113
114
        return mapping
115
116
    tag = "tag:yaml.org,2002:python/unicode"
117
118
    def construct_ustr(loader, node):
119
        """Unicode string constructor"""
120
        return loader.construct_scalar(node)
121
122
    try:
123
        loader.add_constructor(tag, construct_ustr)
124
    except NameError:
125
        pass
126
127
    if type(container) != dict:
128
        loader.add_constructor(mapping_tag, construct_mapping)
129
    return loader
130
131
132
def _customized_dumper(container, dumper=Dumper):
133
    """
134
    Coutnerpart of :func:`_customized_loader` for dumpers.
135
    """
136
    def container_representer(dumper, data, mapping_tag=_MAPPING_TAG):
137
        """Container representer.
138
        """
139
        return dumper.represent_mapping(mapping_tag, data.items())
140
141
    def ustr_representer(dumper, data):
142
        """Unicode string representer"""
143
        tag = "tag:yaml.org,2002:python/unicode"
144
        return dumper.represent_scalar(tag, data)
145
146
    try:
147
        dumper.add_representer(unicode, ustr_representer)
148
    except NameError:
149
        pass
150
151
    if type(container) != dict:
152
        dumper.add_representer(container, container_representer)
153
    return dumper
154
155
156
def _yml_fnc(fname, *args, **options):
157
    """An wrapper of yaml.safe_load, yaml.load, yaml.safe_dump and yaml.dump.
158
159
    :param fname:
160
        "load" or "dump", not checked but it should be OK.
161
        see also :func:`_yml_load` and :func:`_yml_dump`
162
    :param args: [stream] for load or [cnf, stream] for dump
163
    :param options: keyword args may contain "ac_safe" to load/dump safely
164
    """
165
    key = "ac_safe"
166
    fnc = getattr(yaml, r"safe_" + fname if options.get(key) else fname)
167
    return fnc(*args, **_filter_from_options(key, options))
168
169
170
def _yml_load(stream, container, **options):
171
    """An wrapper of yaml.safe_load and yaml.load.
172
173
    :param stream: a file or file-like object to load YAML content
174
    :param container: callble to make a container object
175
176
    :return: Mapping object
177
    """
178
    if options.get("ac_safe", False):
179
        options = {}  # yaml.safe_load does not process Loader opts.
180
    elif not options.get("Loader"):
181
        maybe_container = options.get("ac_dict", False)
182
        if maybe_container and callable(maybe_container):
183
            container = maybe_container
184
185
        options["Loader"] = _customized_loader(container)
186
187
    ret = _yml_fnc("load", stream, **_filter_from_options("ac_dict", options))
188
    if ret is None:
189
        return container()
190
191
    return ret
192
193
194
def _yml_dump(data, stream, **options):
195
    """An wrapper of yaml.safe_dump and yaml.dump.
196
197
    :param data: Some data to dump
198
    :param stream: a file or file-like object to dump YAML data
199
    """
200
    _is_dict = anyconfig.utils.is_dict_like(data)
201
202
    if options.get("ac_safe", False):
203
        options = {}
204
    elif not options.get("Dumper", False) and _is_dict:
205
        # TODO: Any other way to get its constructor?
206
        maybe_container = options.get("ac_dict", type(data))
207
        options["Dumper"] = _customized_dumper(maybe_container)
208
209
    if _is_dict:
210
        # Type information and the order of items are lost on dump currently.
211
        data = anyconfig.dicts.convert_to(data, ac_dict=dict)
212
        options = _filter_from_options("ac_dict", options)
213
214
    return _yml_fnc("dump", data, stream, **options)
215
216
217
class Parser(anyconfig.backend.base.StreamParser):
218
    """
219
    Parser for YAML files.
220
    """
221
    _type = "yaml"
222
    _extensions = ["yaml", "yml"]
223
    _load_opts = ["Loader", "ac_safe", "ac_dict"]
224
    _dump_opts = ["stream", "ac_safe", "Dumper", "default_style",
225
                  "default_flow_style", "canonical", "indent", "width",
226
                  "allow_unicode", "line_break", "encoding", "explicit_start",
227
                  "explicit_end", "version", "tags"]
228
    _ordered = True
229
    _allow_primitives = True
230
    _dict_opts = ["ac_dict"]
231
232
    load_from_stream = anyconfig.backend.base.to_method(_yml_load)
233
    dump_to_stream = anyconfig.backend.base.to_method(_yml_dump)
234
235
# vim:sw=4:ts=4:et:
236