Completed
Push — master ( aa2661...b2c6ce )
by Satoru
01:21
created

m9dicts.make()   D

Complexity

Conditions 8

Size

Total Lines 25

Duplication

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