Completed
Push — master ( d3ddc5...42856b )
by Satoru
01:07
created

m9dicts.get()   B

Complexity

Conditions 6

Size

Total Lines 35

Duplication

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