Completed
Push — master ( 4066ac...7f7dd4 )
by Satoru
01:04
created

m9dicts.convert_to()   B

Complexity

Conditions 5

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 5
dl 0
loc 32
rs 8.0896
1
#
2
# Copyright (C) 2011 - 2015 Red Hat, Inc.
3
# Copyright (C) 2011 - 2016 Satoru SATOH <ssato redhat.com>
4
# License: MIT
5
#
6
"""Functions operate on m9dicts objects.
7
8
.. versionchanged: 0.1.0
9
10
   - splitted / forked from python-anyconfig; old history was available in its
11
     mdict branch:
12
     https://github.com/ssato/python-anyconfig/blob/mdict/anyconfig/mergeabledict.py
13
14
"""
15
from __future__ import absolute_import
16
17
import collections
18
import functools
19
import operator
20
import re
21
22
import m9dicts.compat
23
import m9dicts.globals
24
import m9dicts.dicts
25
import m9dicts.utils
26
27
28
PATH_SEPS = ('/', '.')
29
30
_JSNP_GET_ARRAY_IDX_REG = re.compile(r"(?:0|[1-9][0-9]*)")
31
_JSNP_SET_ARRAY_IDX = re.compile(r"(?:0|[1-9][0-9]*|-)")
32
33
34
def _jsnp_unescape(jsn_s):
35
    """
36
    Parse and decode given encoded JSON Pointer expression, convert ~1 to
37
    / and ~0 to ~.
38
39
    .. note:: JSON Pointer: http://tools.ietf.org/html/rfc6901
40
41
    >>> _jsnp_unescape("/a~1b")
42
    '/a/b'
43
    >>> _jsnp_unescape("~1aaa~1~0bbb")
44
    '/aaa/~bbb'
45
    """
46
    return jsn_s.replace('~1', '/').replace('~0', '~')
47
48
49
def _split_path(path, seps=PATH_SEPS):
50
    """
51
    Parse path expression and return list of path items.
52
53
    :param path: Path expression may contain separator chars.
54
    :param seps: Separator char candidates.
55
    :return: A list of keys to fetch object[s] later.
56
57
    >>> assert _split_path('') == []
58
    >>> assert _split_path('/') == ['']  # JSON Pointer spec expects this.
59
    >>> for p in ('/a', '.a', 'a', 'a.'):
60
    ...     assert _split_path(p) == ['a'], p
61
    >>> assert _split_path('/a/b/c') == _split_path('a.b.c') == ['a', 'b', 'c']
62
    >>> assert _split_path('abc') == ['abc']
63
    """
64
    if not path:
65
        return []
66
67
    for sep in seps:
68
        if sep in path:
69
            if path == sep:  # Special case, '/' or '.' only.
70
                return ['']
71
            return [x for x in path.split(sep) if x]
72
73
    return [path]
74
75
76
def mk_nested_dic(path, val, seps=PATH_SEPS):
77
    """
78
    Make a nested dict iteratively.
79
80
    :param path: Path expression to make a nested dict
81
    :param val: Value to set
82
    :param seps: Separator char candidates
83
84
    >>> mk_nested_dic("a.b.c", 1)
85
    {'a': {'b': {'c': 1}}}
86
    >>> mk_nested_dic("/a/b/c", 1)
87
    {'a': {'b': {'c': 1}}}
88
    """
89
    ret = None
90
    for key in reversed(_split_path(path, seps)):
91
        ret = {key: val if ret is None else ret.copy()}
92
93
    return ret
94
95
96
def get(dic, path, seps=PATH_SEPS, idx_reg=_JSNP_GET_ARRAY_IDX_REG):
97
    """getter for nested dicts.
98
99
    :param dic: a dict[-like] object
100
    :param path: Path expression to point object wanted
101
    :param seps: Separator char candidates
102
    :return: A tuple of (result_object, error_message)
103
104
    >>> d = {'a': {'b': {'c': 0, 'd': [1, 2]}}, '': 3}
105
    >>> assert get(d, '/') == (3, '')  # key becomes '' (empty string).
106
    >>> assert get(d, "/a/b/c") == (0, '')
107
    >>> sorted(get(d, "a.b")[0].items())
108
    [('c', 0), ('d', [1, 2])]
109
    >>> (get(d, "a.b.d"), get(d, "/a/b/d/1"))
110
    (([1, 2], ''), (2, ''))
111
    >>> get(d, "a.b.key_not_exist")  # doctest: +ELLIPSIS
112
    (None, "'...'")
113
    >>> get(d, "/a/b/d/2")
114
    (None, 'list index out of range')
115
    >>> get(d, "/a/b/d/-")  # doctest: +ELLIPSIS
116
    (None, 'list indices must be integers...')
117
    """
118
    items = [_jsnp_unescape(p) for p in _split_path(path, seps)]
119
    if not items:
120
        return (dic, '')
121
    try:
122
        if len(items) == 1:
123
            return (dic[items[0]], '')
124
125
        prnt = functools.reduce(operator.getitem, items[:-1], dic)
126
        arr = m9dicts.utils.is_list_like(prnt) and idx_reg.match(items[-1])
127
        return (prnt[int(items[-1])], '') if arr else (prnt[items[-1]], '')
128
129
    except (TypeError, KeyError, IndexError) as exc:
130
        return (None, str(exc))
131
132
133
def set_(dic, path, val, seps=PATH_SEPS):
134
    """setter for nested dicts.
135
136
    :param dic: a dict[-like] object support recursive merge operations
137
    :param path: Path expression to point object wanted
138
    :param seps: Separator char candidates
139
140
    >>> d = dict(a=1, b=dict(c=2, ))
141
    >>> set_(d, 'a.b.d', 3)
142
    >>> d['a']['b']['d']
143
    3
144
    """
145
    dic.update(mk_nested_dic(path, val, seps))
146
147
148
def check_merge(merge):
149
    """Check if given `merge` is valid and ValueError will be raised if not.
150
    """
151
    if merge not in m9dicts.globals.MERGE_STRATEGIES:
152
        raise ValueError("Wrong merge strategy: %r" % merge)
153
154
155
def _make_from_namedtuple(obj, merge=m9dicts.globals.MS_DICTS,
156
                          _ntpl_cls_key=m9dicts.globals.NTPL_CLS_KEY,
157
                          **options):
158
    """
159
    :param obj: A namedtuple object
160
    :param merge:
161
        Specify strategy from MERGE_STRATEGIES of how to merge results loaded
162
        from multiple configuration files.
163
    :param _ntpl_cls_key:
164
        Special keyword to embedded the class name of namedtuple object to the
165
        MergeableDict object created. It's a hack and not elegant but I don't
166
        think there are another ways to make same namedtuple object from the
167
        MergeableDict object created from it.
168
    """
169
    ocls = m9dicts.dicts.get_mdict_class(merge=merge, ordered=True)
170
    mdict = ocls((k, make(getattr(obj, k), **options)) for k in obj._fields)
171
    mdict[_ntpl_cls_key] = obj.__class__.__name__
172
173
    return mdict
174
175
176
def _make_recur(obj, cls, make_fn, **options):
177
    """
178
    :param obj: An original mapping object
179
    :param cls: Another mapping class to make/convert to
180
    :param make_fn: Function to make/convert to
181
    """
182
    return cls((k, None if v is None else make_fn(v, **options))
183
               for k, v in obj.items())
184
185
186
def _make_iter(obj, make_fn, **options):
187
    """
188
    :param obj: An original mapping object
189
    :param make_fn: Function to make/convert to
190
    """
191
    return type(obj)(make_fn(v, **options) for v in obj)
192
193
194
def make(obj=None, ordered=False, merge=m9dicts.globals.MS_DICTS, **options):
195
    """
196
    Factory function to create a dict-like object[s] supports merge operation
197
    from a mapping or a list of mapping objects such as dict, [dict],
198
    namedtuple, [namedtuple].
199
200
    :param obj: A dict or other object[s] or None
201
    :param ordered:
202
        Choose the class keeps key order if True or `obj` is a namedtuple.
203
    :param merge: see :func:`_make_from_namedtuple` (above).
204
    :return: A dict-like object[s] supports merge operation or `obj` itself
205
    """
206
    check_merge(merge)
207
    cls = m9dicts.dicts.get_mdict_class(merge=merge, ordered=ordered)
208
    if obj is None:
209
        return cls()
210
211
    options.update(ordered=ordered, merge=merge)
212
    if m9dicts.utils.is_dict_like(obj):
213
        return _make_recur(obj, cls, make, **options)
214
    elif m9dicts.utils.is_namedtuple(obj):
215
        return _make_from_namedtuple(obj, **options)
216
    elif m9dicts.utils.is_list_like(obj):
217
        return _make_iter(obj, make, **options)
218
    else:
219
        return obj
220
221
222
def _convert_to_namedtuple(obj, _ntpl_cls_key=m9dicts.globals.NTPL_CLS_KEY,
223
                           **options):
224
    """Convert a dict-like object to a namedtuple.
225
226
    :param obj: A m9dicts objects or other primitive object
227
    :param _ntpl_cls_key: see :func:`_make_from_namedtuple`
228
    """
229
    _name = obj.get(_ntpl_cls_key, "NamedTuple")
230
    _keys = [k for k in obj.keys() if k != _ntpl_cls_key]
231
    _vals = [convert_to(obj[k], **options) for k in _keys]
232
    return collections.namedtuple(_name, _keys)(*_vals)
233
234
235
def convert_to(obj, ordered=False, to_namedtuple=False, **options):
236
    """
237
    Convert a dict-like object[s] support merge operation to a dict or
238
    namedtuple object recursively. Borrowed basic idea and implementation from
239
    bunch.unbunchify. (bunch is distributed under MIT license same as this.)
240
241
    .. note::
242
       - Given `obj` doesn't keep key order and if `to_namedtuple` is True,
243
         then the order of fields of result namedtuple object becomes random.
244
       - namedtuple object cannot have fields start with '_', So it'll fail if
245
         to convert dicts has such keys.
246
247
    :param obj: A m9dicts objects or other primitive object
248
    :param ordered: Create an OrderedDict instead of dict to keep the key order
249
    :param to_namedtuple: Convert `obj` to namedtuple instead of a dict
250
    :param options:
251
        Optional keyword arguments such as _ntpl_cls_key. see
252
        :func:`_make_from_namedtuple` for more its details.
253
254
    :return: A dict or namedtuple object if to_namedtuple is True
255
    """
256
    options.update(ordered=ordered, to_namedtuple=to_namedtuple)
257
    if m9dicts.utils.is_dict_like(obj):
258
        if to_namedtuple:
259
            return _convert_to_namedtuple(obj, **options)
260
        else:
261
            cls = m9dicts.compat.OrderedDict if ordered else dict
262
            return _make_recur(obj, cls, convert_to, **options)
263
    elif m9dicts.utils.is_list_like(obj):
264
        return _make_iter(obj, convert_to, **options)
265
    else:
266
        return obj
267
268
# vim:sw=4:ts=4:et:
269