Completed
Push — master ( 8b294e...44115e )
by Satoru
56s
created

m9dicts._make_from_namedtuple()   A

Complexity

Conditions 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 18
rs 9.4286
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 _make_from_namedtuple(obj, merge=m9dicts.globals.MS_DICTS,
151
                          _ntpl_cls_key=NAMEDTUPLE_CLS_KEY, **opts):
152
    """
153
    :param obj: A namedtuple object
154
    :param merge:
155
        Specify strategy from MERGE_STRATEGIES of how to merge results loaded
156
        from multiple configuration files.
157
    :param _ntpl_cls_key:
158
        Special keyword to embedded the class name of namedtuple object to the
159
        MergeableDict object created. It's a hack and not elegant but I don't
160
        think there are another ways to make same namedtuple object from the
161
        MergeableDict object created from it.
162
    """
163
    ocls = m9dicts.dicts.get_mdict_class(merge=merge, ordered=True)
164
    mdict = ocls((k, make(getattr(obj, k), **opts)) for k in obj._fields)
165
    mdict[_ntpl_cls_key] = obj.__class__.__name__
166
167
    return mdict
168
169
170
def make(obj=None, ordered=False, merge=m9dicts.globals.MS_DICTS,
171
         _ntpl_cls_key=NAMEDTUPLE_CLS_KEY, **options):
172
    """Factory function to create a dict-like object[s] supports merge
173
    operation from a dict or any other objects.
174
175
    :param obj: A dict or other object[s] or None
176
    :param ordered:
177
        Create an instance of OrderedMergeableDict instead of MergeableDict If
178
        it's True. Please note that OrderedMergeableDict class will be chosen
179
        for namedtuple objects regardless of this argument always to keep keys
180
        (fields) order.
181
    :param merge: see :func:`_make_from_namedtuple` (above).
182
    :param _ntpl_cls_key: see :func:`_make_from_namedtuple` (above).
183
    """
184
    if merge not in m9dicts.globals.MERGE_STRATEGIES:
185
        raise ValueError("Wrong merge strategy: %r" % merge)
186
187
    cls = m9dicts.dicts.get_mdict_class(merge=merge, ordered=ordered)
188
    if obj is None:
189
        return cls()
190
191
    opts = dict(ordered=ordered, merge=merge, _ntpl_cls_key=_ntpl_cls_key)
192
    opts.update(options)
193
194
    if m9dicts.utils.is_dict_like(obj):
195
        return cls((k, None if v is None else make(v, **opts)) for k, v
196
                   in obj.items())
197
    elif m9dicts.utils.is_namedtuple(obj):
198
        return _make_from_namedtuple(obj, **opts)
199
    elif m9dicts.utils.is_list_like(obj):
200
        return type(obj)(make(v, **opts) for v in obj)
201
    else:
202
        return obj
203
204
205
def convert_to(obj, ordered=False, to_namedtuple=False,
206
               _ntpl_cls_key=NAMEDTUPLE_CLS_KEY, **opts):
207
    """Convert a dict-like object[s] support merge operation to a dict or
208
    namedtuple object recursively. Borrowed basic idea and implementation from
209
    bunch.unbunchify. (bunch is distributed under MIT license same as this.)
210
211
    .. note::
212
       - Given `obj` doesn't keep key order and if `to_namedtuple` is True,
213
         then the order of fields of result namedtuple object becomes random.
214
       - namedtuple object cannot have fields start with '_', So it'll fail if
215
         to convert dicts has such keys.
216
217
    :param obj: A m9dicts objects or other primitive object
218
    :param ordered: Create an OrderedDict instead of dict to keep the key order
219
    :param to_namedtuple:
220
        Convert `obj` to namedtuple object of which definition is created on
221
        the fly if True instead of dict.
222
    :param _ntpl_cls_key: see :func:`_make_from_namedtuple`
223
224
    :return: A dict or namedtuple object if to_namedtuple is True
225
    """
226
    cls = m9dicts.compat.OrderedDict if ordered else dict
227
    opts.update(ordered=ordered, to_namedtuple=to_namedtuple,
228
                _ntpl_cls_key=_ntpl_cls_key)
229
    if m9dicts.utils.is_dict_like(obj):
230
        if not to_namedtuple:
231
            return cls((k, convert_to(v, **opts)) for k, v in obj.items())
232
233
        _name = obj.get(_ntpl_cls_key, "NamedTuple")
234
        _keys = [k for k in obj.keys() if k != _ntpl_cls_key]
235
        _vals = [convert_to(obj[k], **opts) for k in _keys]
236
        return collections.namedtuple(_name, _keys)(*_vals)
237
    elif m9dicts.utils.is_list_like(obj):
238
        return type(obj)(convert_to(v, **opts) for v in obj)
239
    else:
240
        return obj
241
242
# vim:sw=4:ts=4:et:
243