Statistics
| Branch: | Tag: | Revision:

root / snf-common / synnefo / util / keypath.py @ 5858e64a

History | View | Annotate | Download (8.5 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34

    
35
import re
36
integer_re = re.compile('-?[0-9]+')
37

    
38

    
39
def join_path(sep, path):
40
    iterable = ((str(n) if isinstance(n, (int, long)) else n) for n in path)
41
    return sep.join(iterable)
42

    
43

    
44
def lookup_path(container, path, sep='.', createpath=False):
45
    """
46
    return (['a','b'],
47
            [container['a'], container['a']['b']],
48
            'c')  where path=sep.join(['a','b','c'])
49

50
    """
51
    names = path.split(sep)
52
    dirnames = names[:-1]
53
    basename = names[-1]
54
    if integer_re.match(basename):
55
        basename = int(basename)
56

    
57
    node = container
58
    name_path = []
59
    node_path = [node]
60
    for name in dirnames:
61
        name_path.append(name)
62

    
63
        if integer_re.match(name):
64
            name = int(name)
65

    
66
        try:
67
            node = node[name]
68
        except KeyError as e:
69
            if not createpath:
70
                m = "'{0}': path not found".format(join_path(sep, name_path))
71
                raise KeyError(m)
72
            node[name] = {}
73
            node = node[name]
74
        except IndexError as e:
75
            if not createpath:
76
                m = "'{0}': path not found: {1}".format(
77
                    join_path(sep, name_path), e)
78
                raise KeyError(m)
79
            size = name if name > 0 else -name
80
            node += (dict() for _ in xrange(len(node), size))
81
            node = node[name]
82
        except TypeError as e:
83
            m = "'{0}': cannot traverse path beyond this node: {1}"
84
            m = m.format(join_path(sep, name_path), str(e))
85
            raise ValueError(m)
86
        node_path.append(node)
87

    
88
    return name_path, node_path, basename
89

    
90

    
91
def walk_paths(container):
92
    for name, node in container.iteritems():
93
        if not hasattr(node, 'items'):
94
            yield [name], [node]
95
        else:
96
            for names, nodes in walk_paths(node):
97
                yield [name] + names, [node] + nodes
98

    
99

    
100
def list_paths(container, sep='.'):
101
    """
102
    >>> sorted(list_paths({'a': {'b': {'c': 'd'}}}))
103
    [('a.b.c', 'd')]
104
    >>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': 3}}))
105
    [('a.b.c', 'd'), ('a.e', 3)]
106
    >>> sorted(list_paths({'a': {'b': {'c': 'd'}, 'e': {'f': 3}}}))
107
    [('a.b.c', 'd'), ('a.e.f', 3)]
108
    >>> sorted(list_paths({'a': [{'b': 3}, 2]}))
109
    [('a', [{'b': 3}, 2])]
110
    >>> list_paths({})
111
    []
112

113
    """
114
    return [(join_path(sep, name_path), node_path[-1])
115
            for name_path, node_path in walk_paths(container)]
116

    
117

    
118
def del_path(container, path, sep='.', collect=True):
119
    """
120
    del container['a']['b']['c'] where path=sep.join(['a','b','c'])
121

122
    >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c'); d
123
    {}
124
    >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c', collect=False); d
125
    {'a': {'b': {}}}
126
    >>> d = {'a': {'b': {'c': 'd'}}}; del_path(d, 'a.b.c.d')
127
    Traceback (most recent call last):
128
    ValueError: 'a.b.c': cannot traverse path beyond this node:\
129
 'str' object does not support item deletion
130
    """
131

    
132
    name_path, node_path, basename = \
133
            lookup_path(container, path, sep=sep, createpath=False)
134

    
135
    lastnode = node_path.pop()
136
    lastname = basename
137
    try:
138
        if basename in lastnode:
139
            del lastnode[basename]
140
    except (TypeError, KeyError) as e:
141
        m = "'{0}': cannot traverse path beyond this node: {1}"
142
        m = m.format(join_path(sep, name_path), str(e))
143
        raise ValueError(m)
144

    
145
    if collect:
146
        while node_path and not lastnode:
147
            basename = name_path.pop()
148
            lastnode = node_path.pop()
149
            del lastnode[basename]
150

    
151

    
152
def get_path(container, path, sep='.'):
153
    """
154
    return container['a']['b']['c'] where path=sep.join(['a','b','c'])
155

156
    >>> get_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d')
157
    Traceback (most recent call last):
158
    ValueError: 'a.b.c.d': cannot traverse path beyond this node:\
159
 string indices must be integers, not str
160
    >>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c.d')
161
    Traceback (most recent call last):
162
    ValueError: 'a.b.c.d': cannot traverse path beyond this node:\
163
 'int' object is unsubscriptable
164
    >>> get_path({'a': {'b': {'c': 1}}}, 'a.b.c')
165
    1
166
    >>> get_path({'a': {'b': {'c': 1}}}, 'a.b')
167
    {'c': 1}
168
    >>> get_path({'a': [{'z': 1}]}, 'a.0')
169
    {'z': 1}
170
    >>> get_path({'a': [{'z': 1}]}, 'a.0.z')
171
    1
172
    >>> get_path({'a': [{'z': 1}]}, 'a.-1.z')
173
    1
174

175
    """
176
    name_path, node_path, basename = \
177
            lookup_path(container, path, sep=sep, createpath=False)
178
    name_path.append(basename)
179
    node = node_path[-1]
180

    
181
    try:
182
        return node[basename]
183
    except TypeError as e:
184
        m = "'{0}': cannot traverse path beyond this node: {1}"
185
        m = m.format(join_path(sep, name_path), str(e))
186
        raise ValueError(m)
187
    except KeyError as e:
188
        m = "'{0}': path not found: {1}"
189
        m = m.format(join_path(sep, name_path), str(e))
190
        raise KeyError(m)
191

    
192

    
193
def set_path(container, path, value, sep='.',
194
             createpath=False, overwrite=True):
195
    """
196
    container['a']['b']['c'] = value where path=sep.join(['a','b','c'])
197

198
    >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c.d', 1)
199
    Traceback (most recent call last):
200
    ValueError: 'a.b.c.d': cannot index non-object node with string
201
    >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1)
202
    Traceback (most recent call last):
203
    KeyError: "'a.b.x': path not found"
204
    >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.x.d', 1, createpath=True)
205
    
206
    >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1)
207
    
208
    >>> set_path({'a': {'b': {'c': 'd'}}}, 'a.b.c', 1, overwrite=False)
209
    Traceback (most recent call last):
210
    ValueError: will not overwrite path 'a.b.c'
211
    >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.1', 2, createpath=False)
212
    Traceback (most recent call last):
213
    KeyError: "'a.-2': path not found: list index out of range"
214
    >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.1', 2, createpath=True)
215
    Traceback (most recent call last):
216
    ValueError: 'a.-2.1': will not index object node with integer
217
    >>> d = {'a': [{'z': 1}]}; set_path(d, 'a.-2.z', 2, createpath=True); \
218
 d['a'][-2]['z']
219
    2
220

221
    """
222
    name_path, node_path, basename = \
223
            lookup_path(container, path, sep=sep, createpath=createpath)
224
    name_path.append(basename)
225
    node = node_path[-1]
226

    
227
    if basename in node and not overwrite:
228
        m = "will not overwrite path '{0}'".format(path)
229
        raise ValueError(m)
230

    
231
    is_object_node = hasattr(node, 'keys')
232
    is_string_name = isinstance(basename, basestring)
233
    if not is_string_name and is_object_node:
234
        m = "'{0}': will not index object node with integer"
235
        m = m.format(join_path(sep, name_path))
236
        raise ValueError(m)
237
    if is_string_name and not is_object_node:
238
        m = "'{0}': cannot index non-object node with string"
239
        m = m.format(join_path(sep, name_path))
240
        raise ValueError(m)
241
    try:
242
        node[basename] = value
243
    except TypeError as e:
244
        m = "'{0}': cannot traverse path beyond this node: {1}"
245
        m = m.format(join_path(sep, name_path), str(e))
246
        raise ValueError(m)
247

    
248

    
249
if __name__ == '__main__':
250
    import doctest
251
    doctest.testmod()