tree.py 23.6 KB
Newer Older
Enrico Zini's avatar
Enrico Zini committed
1
2
# coding: utf-8
from __future__ import (absolute_import, print_function, division, unicode_literals)
Enrico Zini's avatar
Enrico Zini committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os.path
import itertools
import re

_tolist_split = re.compile(r'/+')

def _tolist(path):
    """
    Split a path into a list of components
    """
    path = path.strip('/')
    if path == "": return []
    return _tolist_split.split(path)

Enrico Zini's avatar
Enrico Zini committed
17

Enrico Zini's avatar
Enrico Zini committed
18
19
class Tree(object):
    """
Enrico Zini's avatar
Enrico Zini committed
20
21
22
23
24
    Generic interface for tree operations.

    Methods prefixed with 'l' take a list of split path components as a path
    element, their equivalent without the leading 'l' take a string as a path
    component.
Enrico Zini's avatar
Enrico Zini committed
25
    """
Enrico Zini's avatar
Enrico Zini committed
26
    def __init__(self, name=None, doc=None):
Enrico Zini's avatar
Enrico Zini committed
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
        """
        Name is the name of this tree.  This allows to use the tree as a
        subtree inside another, and still get meaningful exception messages.
        """
        self._name = name or "/"
        self._documentation = doc

    def has(self, path, **kw):
        """
        Return True if the path exists, else False
        """
        return self.lhas(_tolist(path), **kw)

    def lhas(self, path, **kw):
        """
        Return True if the path exists, else False
        """
        return False

    def get(self, path, **kw):
        """
        Returns the object at the given path, or None if path does not exist
        """
        return self.lget(_tolist(path), **kw)

    def lget(self, path, **kw):
        """
        Returns the object at the given path, or None if path does not exist
        """
        return None

    def set(self, path, value, **kw):
        """
        Update the object at the given path using the ``value`` object.
        """
        return self.lset(_tolist(path), value, **kw)

    def lset(self, path, value, **kw):
        """
        Update the object at the given path using the ``value`` object.
        """
Enrico Zini's avatar
Enrico Zini committed
68
        raise KeyError("cannot set the value of path %s" % os.path.join(self._name, *path))
Enrico Zini's avatar
Enrico Zini committed
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

    def list(self, path, **kw):
        """
        Return a sequence with the children of the object at the given path.

        The elements of the sequence can be the same objects that would be
        returned by issuing a ``get`` on each of the children, but can also be
        lightweight "summary" objects (for example, a string with the name of
        the child node) in those cases where instantiating an object for every
        child would be overkill for a list operation.
        """
        return self.llist(_tolist(path), **kw)

    def llist(self, path, **kw):
        """
        Return a sequence with the children of the object at the given path.

        The elements of the sequence can be the same objects that would be
        returned by issuing a ``get`` on each of the children, but can also be
        lightweight "summary" objects (for example, a string with the name of
        the child node) in those cases where instantiating an object for every
        child would be overkill for a list operation.
        """
Enrico Zini's avatar
Enrico Zini committed
92
        raise KeyError("cannot list the children of path %s" % os.path.join(self._name, *path))
Enrico Zini's avatar
Enrico Zini committed
93
94
95
96
97
98
99
100
101
102
103

    def delete(self, path, **kw):
        """
        Delete the object at the given path, and all its subobjects
        """
        return self.ldelete(_tolist(path), **kw)

    def ldelete(self, path, **kw):
        """
        Delete the object at the given path, and all its subobjects
        """
Enrico Zini's avatar
Enrico Zini committed
104
        raise KeyError("cannot delete path %s" % os.path.join(self._name, *path))
Enrico Zini's avatar
Enrico Zini committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

    def create(self, path, value=None, **kw):
        """
        Create the object at the given path.

        If value is given, it is used to initialise the object as if a set had
        been done with that value.  Otherwise, the object is created using
        default values.
        """
        return self.lcreate(_tolist(path), value)

    def lcreate(self, path, value=None):
        """
        Create the object at the given path.

        If value is given, it is used to initialise the object as if a set had
        been done with that value.  Otherwise, the object is created using
        default values.
        """
Enrico Zini's avatar
Enrico Zini committed
124
        raise KeyError("cannot create path %s" % os.path.join(self._name, *path))
Enrico Zini's avatar
Enrico Zini committed
125
126
127
128
129
130
131
132

    def doc(self, path, **kw):
        """
        Return documentation for the given path.

        Returns None if no documentation is available
        """
        return self.ldoc(_tolist(path), **kw)
Enrico Zini's avatar
Enrico Zini committed
133

Enrico Zini's avatar
Enrico Zini committed
134
135
136
137
138
139
140
141
    def ldoc(self, path, **kw):
        """
        Return documentation for the given path.

        Returns None if no documentation is available
        """
        return self._documentation

Enrico Zini's avatar
Enrico Zini committed
142

Enrico Zini's avatar
Enrico Zini committed
143
144
class Subtree(Tree):
    """
Enrico Zini's avatar
Enrico Zini committed
145
    A tree that gives proxy access to a subtree of another tree
Enrico Zini's avatar
Enrico Zini committed
146
147
148
149
150
151
    """
    def __init__(self, tree, root):
        root = root.strip('/')
        super(Subtree, self).__init__(os.path.join(tree._name, root))
        self.tree = tree
        self.root = _tolist(root)
Enrico Zini's avatar
Enrico Zini committed
152

Enrico Zini's avatar
Enrico Zini committed
153
154
    def lhas(self, path, **kw):
        return self.tree.lhas(self.root + path, **kw)
Enrico Zini's avatar
Enrico Zini committed
155

Enrico Zini's avatar
Enrico Zini committed
156
157
    def lget(self, path, **kw):
        return self.tree.lget(self.root + path, **kw)
Enrico Zini's avatar
Enrico Zini committed
158

Enrico Zini's avatar
Enrico Zini committed
159
160
    def lset(self, path, value, **kw):
        self.tree.lset(self.root + path, value, **kw)
Enrico Zini's avatar
Enrico Zini committed
161

Enrico Zini's avatar
Enrico Zini committed
162
163
    def llist(self, path, **kw):
        return self.tree.llist(self.root + path, **kw)
Enrico Zini's avatar
Enrico Zini committed
164

Enrico Zini's avatar
Enrico Zini committed
165
166
    def ldelete(self, path, **kw):
        return self.tree.ldelete(self.root + path, **kw)
Enrico Zini's avatar
Enrico Zini committed
167

Enrico Zini's avatar
Enrico Zini committed
168
169
    def lcreate(self, path, value=None, **kw):
        return self.tree.lcreate(self.root + path, value, **kw)
Enrico Zini's avatar
Enrico Zini committed
170

Enrico Zini's avatar
Enrico Zini committed
171
172
173
    def ldoc(self, path, **kw):
        return self.tree.ldoc(self.root + path, **kw)

Enrico Zini's avatar
Enrico Zini committed
174

Enrico Zini's avatar
Enrico Zini committed
175
176
177
178
179
class Forest(Tree):
    """
    Maintains a virtual tree of tree items, and dispatches requests to the tree
    items that are registered
    """
Enrico Zini's avatar
Enrico Zini committed
180
    def __init__(self, name="/", doc=None):
Enrico Zini's avatar
Enrico Zini committed
181
182
183
184
185
        """
        Name is the name of this forest.  This allows to use a forest as a
        subtree inside another, and still get meaningful exception messages.
        """
        super(Forest, self).__init__(name, doc)
Enrico Zini's avatar
Enrico Zini committed
186
        self.branches = {}
Enrico Zini's avatar
Enrico Zini committed
187
188
189
190
191
192
193
194
195
196
197

    def _get(self, **kw):
        """
        Get the value associated with this tree node
        """
        return None

    def _set(self, value, **kw):
        """
        Set the value associated with this tree node
        """
198
        raise KeyError("cannot set the value of path %s" % self._name)
Enrico Zini's avatar
Enrico Zini committed
199

Enrico Zini's avatar
Enrico Zini committed
200
    def register(self, tree):
Enrico Zini's avatar
Enrico Zini committed
201
202
203
        """
        Register the given tree object to answer queries at the given branch
        """
Enrico Zini's avatar
Enrico Zini committed
204
        self.branches[tree._name] = tree
Enrico Zini's avatar
Enrico Zini committed
205
206
207
208
209
210
211
212
213
214

    def branch(self, branch):
        return self.branches.get(branch, None)

    def lhas(self, path, **kw):
        """
        Return True if the path exists, else False
        """
        if len(path) == 0: return True
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
215
        if tree is None: return False
Enrico Zini's avatar
Enrico Zini committed
216
217
218
219
220
221
222
223
224
        if len(path) == 1: return True
        return tree.lhas(path[1:], **kw)

    def lget(self, path, **kw):
        """
        Returns the object at the given path, or None if path does not exist
        """
        if len(path) == 0: return self._get(**kw)
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
225
        if tree is None: return None
Enrico Zini's avatar
Enrico Zini committed
226
227
228
229
230
231
232
233
        return tree.lget(path[1:], **kw)

    def lset(self, path, value, **kw):
        """
        Update the object at the given path using the ``value`` object.
        """
        if len(path) == 0: return self._set(value, **kw)
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
234
        if tree is None: raise KeyError("path {} does not exist".format(os.path.join(self._name, path[0])))
Enrico Zini's avatar
Enrico Zini committed
235
236
237
238
239
240
241
242
243
244
245
        return tree.lset(path[1:], value, **kw)

    def llist(self, path, **kw):
        """
        Return a sequence with the children of the object at the given path.

        The elements of the sequence can be the same objects that would be
        returned by issuing a ``get`` on each of the children, but can also be
        lightweight "summary" objects in those cases where instantiating an
        object for every child would be overkill for a list operation
        """
Christopher R. Gabriel's avatar
Christopher R. Gabriel committed
246
        if len(path) == 0: return list(self.branches.keys())
Enrico Zini's avatar
Enrico Zini committed
247
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
248
        if tree is None: raise KeyError("path %s does not exist" % os.path.join(self._name, path[0]))
Enrico Zini's avatar
Enrico Zini committed
249
250
251
252
253
254
255
256
        return tree.llist(path[1:], **kw)

    def ldelete(self, path, **kw):
        """
        Delete the object at the given path, and all its subobjects
        """
        if len(path) == 0: raise LookupError("tree %s cannot delete self" % self._name)
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
257
        if tree is None: raise KeyError("path %s does not exist" % os.path.join(self._name, path[0]))
258
        if len(path) == 1: raise KeyError("cannot delete path %s" % os.path.join(self._name, path[0]))
Enrico Zini's avatar
Enrico Zini committed
259
260
261
262
263
264
265
266
267
268
269
        tree.ldelete(path[1:], **kw)

    def lcreate(self, path, value=None, **kw):
        """
        Create the object at the given path.

        If value is given, it is used to initialise the object as if a set had
        been done with that value.  Otherwise, the object is created using
        default values.
        """
        if len(path) == 0: raise LookupError("tree %s cannot create self" % self._name)
270
        if len(path) == 1: raise KeyError("path %s cannot be created" % os.path.join(self._name, path[0]))
Enrico Zini's avatar
Enrico Zini committed
271
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
272
        if tree is None: raise KeyError("path %s does not exist" % os.path.join(self._name, path[0]))
Enrico Zini's avatar
Enrico Zini committed
273
274
275
276
277
        return tree.lcreate(path[1:], value, **kw)

    def ldoc(self, path, **kw):
        if len(path) == 0: return super(Forest, self).ldoc(path, **kw)
        tree = self.branches.get(path[0], None)
Enrico Zini's avatar
Enrico Zini committed
278
        if tree is None: return None
Enrico Zini's avatar
Enrico Zini committed
279
280
281
282
283
284
285
286
        return tree.ldoc(path[1:], **kw)


class Parrot(Tree):
    """
    Tree whose every method raises an exception with the method name and
    arguments
    """
Enrico Zini's avatar
Enrico Zini committed
287
    def __init__(self, name="/"):
Enrico Zini's avatar
Enrico Zini committed
288
289
290
291
        super(Parrot, self).__init__(name)

    def lhas(self, path):
        raise Exception("%s: has %s" % (self._name, "/".join(path)))
Enrico Zini's avatar
Enrico Zini committed
292

Enrico Zini's avatar
Enrico Zini committed
293
294
    def lget(self, path):
        raise Exception("%s: get %s" % (self._name, "/".join(path)))
Enrico Zini's avatar
Enrico Zini committed
295

Enrico Zini's avatar
Enrico Zini committed
296
297
    def lset(self, path, value):
        raise Exception("%s: set %s \"%s\"" % (self._name, "/".join(path), value))
Enrico Zini's avatar
Enrico Zini committed
298

Enrico Zini's avatar
Enrico Zini committed
299
300
    def llist(self, path):
        raise Exception("%s: list %s" % (self._name, "/".join(path)))
Enrico Zini's avatar
Enrico Zini committed
301

Enrico Zini's avatar
Enrico Zini committed
302
303
    def ldelete(self, path):
        raise Exception("%s: delete %s" % (self._name, "/".join(path)))
Enrico Zini's avatar
Enrico Zini committed
304

Enrico Zini's avatar
Enrico Zini committed
305
306
    def lcreate(self, path, value=None):
        raise Exception("%s: create %s \"%s\"" % (self._name, "/".join(path), value))
Enrico Zini's avatar
Enrico Zini committed
307

Enrico Zini's avatar
Enrico Zini committed
308
309
310
    def ldoc(self, path):
        raise Exception("%s: doc %s" % (self._name, "/".join(path)))

Enrico Zini's avatar
Enrico Zini committed
311

Enrico Zini's avatar
Enrico Zini committed
312
313
314
315
316
class PyTree(Tree):
    """
    Tree node built using a python class hierarchy, and property-aware
    """

Enrico Zini's avatar
Enrico Zini committed
317
    def __init__(self, name="/", doc=None):
Enrico Zini's avatar
Enrico Zini committed
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
        super(PyTree, self).__init__(name, doc)

    def _asproperty(self, name):
        m = getattr(self.__class__, name, None)
        if m != None and isinstance(m, property):
            return m
        else:
            return None

    def _astree(self, name):
        m = getattr(self, name, None)
        if m != None and isinstance(m, Tree):
            return m
        else:
            return None

    def _has(self, **kw):
        """
        Report if this node exists.

        There is probably no reason for this not to be true, but this is still
        implemented in a hookable way to keep the interface consistent.
        """
        return True

    def _get(self, **kw):
        """
        Get the value associated with this tree node
        """
        return None

    def _set(self, value, **kw):
        """
        Set the value associated with this tree node
        """
353
        raise KeyError("cannot set the value of path %s" % self._name)
Enrico Zini's avatar
Enrico Zini committed
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373

    def _list(self, **kw):
        """
        List the values associated with this tree node
        """
        res = []
        for method in dir(self):
            if self._asproperty(method) or self._astree(method):
                res.append(method)
        return res

    def _delete(self, **kw):
        """
        Delete the object itself.

        Note: this requires cooperation with the upper tree node, and hence it
        probably makes no sense to override this method.  The default
        implementation raises an exception, which is probably good for most of
        the time.
        """
374
        raise KeyError("cannot delete path %s" % self._name)
Enrico Zini's avatar
Enrico Zini committed
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

    def _doc(self, **kw):
        """
        Get the documentation for this tree node
        """
        return super(PyTree, self).ldoc([])

    def lhas(self, path, **kw):
        if len(path) == 0: return self._has(**kw)
        sub = self._astree(path[0])
        if sub: return sub.lhas(path[1:], **kw)
        if len(path) > 1: return False
        sub = self._asproperty(path[0])
        if sub: return True
        return False

    def lget(self, path, **kw):
        if len(path) == 0: return self._get(**kw)
        sub = self._astree(path[0])
        if sub: return sub.lget(path[1:], **kw)
        if len(path) > 1: return None
        sub = self._asproperty(path[0])
        if sub: return sub.__get__(self)
        return None

    def lset(self, path, value, **kw):
        if len(path) == 0: return self._set(value, **kw)
        sub = self._astree(path[0])
        if sub: return sub.lset(path[1:], value, **kw)
        if len(path) > 1: return super(PyTree, self).lset(path, value, **kw)
        sub = self._asproperty(path[0])
        if sub: return sub.__set__(self, value)
        return None

    def llist(self, path, **kw):
        if len(path) == 0: return self._list(**kw)
        sub = self._astree(path[0])
        if sub: return sub.llist(path[1:], **kw)
        if len(path) > 1 or not self._asproperty(path[0]):
            return super(PyTree, self).llist(path, **kw)
        return []

    def ldelete(self, path, **kw):
        if len(path) == 0: return self._delete(**kw)
        sub = self._astree(path[0])
        if sub: return sub.ldelete(path[1:], **kw)
        return super(PyTree, self).ldelete(path, **kw)

    def lcreate(self, path, value=None, **kw):
424
        if len(path) == 0: raise KeyError("cannot create path %s" % self._name)
Enrico Zini's avatar
Enrico Zini committed
425
426
427
428
429
430
431
432
433
434
435
436
437
438
        sub = self._astree(path[0])
        if sub: return sub.lcreate(path[1:], value, **kw)
        return super(PyTree, self).lcreate(path, value, **kw)

    def ldoc(self, path, **kw):
        if len(path) == 0: return self._doc(**kw)
        sub = self._astree(path[0])
        if sub: return sub.ldoc(path[1:], **kw)
        if len(path) > 1: return None
        sub = self._asproperty(path[0])
        if sub: return sub.__doc__
        return None


Enrico Zini's avatar
Enrico Zini committed
439
class ReadonlyDictTree(Forest):
Enrico Zini's avatar
Enrico Zini committed
440
441
442
    """
    dict that is also a readonly tree
    """
Enrico Zini's avatar
Enrico Zini committed
443
444
445
446
447
448
449
450
451
452
453
454
    def __init__(self, name="/", doc=None, *args, **kw):
        super(ReadonlyDictTree, self).__init__(name, doc)
        self._values = dict(*args, **kw)

    def __hasitem__(self, key):
        return self._values.__hasitem__(key)

    def __getitem__(self, key):
        return self._values.__getitem__(key)

    def __setitem__(self, key, val):
        return self._values.__setitem__(key, val)
Enrico Zini's avatar
Enrico Zini committed
455
456

    def ldoc(self, path):
Enrico Zini's avatar
Enrico Zini committed
457
458
459
460
        if len(path) == 0 or path[0] not in self._values: return super(ReadonlyDictTree, self).ldoc(path)
        if len(path) == 1 and path[0] in self._values: return self._documentation + ": " + path[0]
        return super(ReadonlyDictTree, self).ldoc(path)

Enrico Zini's avatar
Enrico Zini committed
461
462
    def lhas(self, path):
        if len(path) == 0: return True
Enrico Zini's avatar
Enrico Zini committed
463
464
465
        if len(path) == 1 and path[0] in self._values: return True
        return super(ReadonlyDictTree, self).lhas(path)

Enrico Zini's avatar
Enrico Zini committed
466
    def lget(self, path):
Enrico Zini's avatar
Enrico Zini committed
467
468
469
470
        if len(path) == 0: return dict(self._values)
        if len(path) == 1 and path[0] in self._values: return self._values[path[0]]
        return super(ReadonlyDictTree, self).lget(path)

Enrico Zini's avatar
Enrico Zini committed
471
472
    def llist(self, path):
        if len(path) == 0:
Enrico Zini's avatar
Enrico Zini committed
473
474
475
476
            return sorted(set(Forest.llist(self, [])) | set(self._values.keys()))
        if len(path) == 1 and path[0] in self._values: return []
        return super(ReadonlyDictTree, self).llist(path)

Enrico Zini's avatar
Enrico Zini committed
477
478
479
480
481
482

class DictTree(ReadonlyDictTree):
    """
    Writable version of ReadonlyDictTree
    """
    def lset(self, path, value):
Enrico Zini's avatar
Enrico Zini committed
483
484
485
486
        if len(path) == 1 and path[0] in self._values:
            self._values[path[0]] = value
        else:
            return super(ReadonlyDictTree, self).lset(path, value)
Enrico Zini's avatar
Enrico Zini committed
487
488

    def ldelete(self, path):
Enrico Zini's avatar
Enrico Zini committed
489
        if len(path) == 1 and path[0] in self._values:
490
            del self._values[path[0]]
Enrico Zini's avatar
Enrico Zini committed
491
492
        else:
            return super(ReadonlyDictTree, self).ldelete(path, value)
Enrico Zini's avatar
Enrico Zini committed
493
494
495

    def lcreate(self, path, value=None):
        if len(path) != 1 or path[0] in self.branches:
Enrico Zini's avatar
Enrico Zini committed
496
497
            return super(ReadonlyDictTree, self).lcreate(path, value)
        else:
498
            self._values[path[0]] = value
Enrico Zini's avatar
Enrico Zini committed
499
500
            return value

Enrico Zini's avatar
Enrico Zini committed
501
502
503
504
505

class DynamicView(Tree):
    """
    Tree that is a view over some dynamic database
    """
Enrico Zini's avatar
Enrico Zini committed
506
    def __init__(self, name, doc=None):
Enrico Zini's avatar
Enrico Zini committed
507
        super(DynamicView, self).__init__(name, doc)
Enrico Zini's avatar
Enrico Zini committed
508
        self.specials = {}
Enrico Zini's avatar
Enrico Zini committed
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526

    def elements(self):
        """
        Generate the sequence of Trees on this view
        """
        return []

    def element(self, name):
        """
        Instantiate one element by name, or return None if the element does not
        exist
        """
        return None

    def new_element(self, name, value):
        """
        Create the element 'name', with the given value
        """
527
        raise KeyError("cannot create element %s in %s" % (name, os.path.join(self._name, name)))
Enrico Zini's avatar
Enrico Zini committed
528
529
530
531
532
533
534
535

    def lhas(self, path):
        if len(path) == 0: return True
        if path[0] in self.specials:
            if len(path) == 1: return True
            return self.specials[path[0]].lhas(path[1:])
        else:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
536
            if el is None: return False
Enrico Zini's avatar
Enrico Zini committed
537
538
539
540
541
542
543
544
545
            if len(path) == 1: return True
            return el.lhas(path[1:])

    def lget(self, path):
        if len(path) == 0: return [e.lget([]) for e in self.elements()]
        if path[0] in self.specials:
            return self.specials[path[0]].lget(path[1:])
        else:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
546
            if el is None: return None
Enrico Zini's avatar
Enrico Zini committed
547
548
549
550
551
552
553
554
            return el.lget(path[1:])

    def lset(self, path, value):
        if len(path) == 0: return super(DynamicView, self).lset(path, value)
        if path[0] in self.specials:
            return self.specials[path[0]].lset(path[1:], value)
        else:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
555
            if el is None: return super(DynamicView, self).lset(path, value)
Enrico Zini's avatar
Enrico Zini committed
556
557
558
559
560
561
562
563
564
            return el.lset(path[1:], value)

    def llist(self, path):
        if len(path) == 0:
            return sorted(self.specials.keys()) + sorted([e._name for e in self.elements()])
        if path[0] in self.specials:
            return self.specials[path[0]].llist(path[1:])
        else:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
565
            if el is None: return super(DynamicView, self).llist(path)
Enrico Zini's avatar
Enrico Zini committed
566
567
568
569
570
571
572
573
574
            return el.llist(path[1:])

    def lcreate(self, path, value=None):
        if len(path) == 0:
            return super(DynamicView, self).lcreate(path, value)
        elif path[0] in self.specials:
            return self.specials[path[0]].lcreate(path[1:], value)
        elif len(path) > 1:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
575
            if el is None: return super(DynamicView, self).lcreate(path, value)
Enrico Zini's avatar
Enrico Zini committed
576
577
578
579
580
581
582
583
584
585
586
            return el.lcreate(path[1:], value)
        else:
            return self.new_element(path[0], value)

    def ldelete(self, path):
        if len(path) == 0:
            return super(DynamicView, self).ldelete(path)
        elif path[0] in self.specials:
            return self.specials[path[0]].ldelete(path[1:])
        elif len(path) > 1:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
587
            if el is None: return super(DynamicView, self).ldelete(path)
Enrico Zini's avatar
Enrico Zini committed
588
589
590
591
592
593
594
595
596
597
            return el.ldelete(path[1:])
        else:
            return self.delete_element(path[0])

    def ldoc(self, path):
        if len(path) == 0: return super(DynamicView, self).ldoc(path)
        if path[0] in self.specials:
            return self.specials[path[0]].ldoc(path[1:])
        else:
            el = self.element(path[0])
Enrico Zini's avatar
Enrico Zini committed
598
            if el is None: return None
Enrico Zini's avatar
Enrico Zini committed
599
600
            return el.ldoc(path[1:])

Enrico Zini's avatar
Enrico Zini committed
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687

class MockTree(Tree):
    """
    A tree containing a whole hierarchy, stored in ram.

    ldoc always returns None

    As a leaf node:
     - self.value = scalar
     - has("") → True
     - get("") → self.value
     - set("", scalar:val) → self.value = val
     - set("", dict:val) → for name, value in val.items(): self.value[name].set("", value)
     - list("") → []
     - create("", val) → error
     - create("foo", val) → self.value is replaced with a dict { "foo": MockTree("foo", None, val) }
     - delete("") → error
     - delete("foo") → error
     - all other combinations → error

    As a tree node:
     - self.value = { name: MockTree(name, None) }
     - has("") → True
     - has("foo/bar") → self.value["foo"].has("bar")
     - get("") → { name, tree.get("") for name, tree in self.value.items }
     - get("foo/bar") → self.value["foo"].get("bar")
     - set("", scalar:val) → self.value = val
     - set("", dict:val) → for name, value in val.items(): self.value[name].set("", value)
     - set("foo/bar", val) → self.value["foo"].set("bar", val)
     - list("") → sorted(self.value.keys())
     - list("foo/bar") → self.value["foo"].list("bar")
     - create("", val) → error
     - create("foo", val) → self.value["foo"] = MockTree("foo", None, val)
     - delete("") → error
     - delete("foo") → del self.value["foo"]
     - delete("foo/bar") → self.value["foo"].delete("bar")
     - all other combinations → error
    """
    def __init__(self, name=None, doc=None, value=None):
        super(MockTree, self).__init__(name, doc)
        # A scalar if this is a leaf node, a { name: MockTree } if this node has children
        self.value = value

    def _forward(self, method, path, *args, **kw):
        subtree = self.value.get(path[0], None) if not self.is_leaf else None
        if subtree is None:
            return getattr(super(MockTree, self), method)(path, *args, **kw)
        else:
            return getattr(subtree, method)(path[1:], *args, **kw)

    @property
    def is_leaf(self):
        return not isinstance(self.value, dict)

    def lhas(self, path):
        if not path: return True
        return self._forward("lhas", path)

    def lget(self, path):
        if path: return self._forward("lget", path)
        if self.is_leaf:
            return self.value
        else:
            return { name: subtree.lget([]) for name, subtree in self.value.items() }

    def lset(self, path, value):
        if path: return self._forward("lset", path, value)
        if isinstance(value, dict):
            self.value = {}
            for name, val in value.items():
                subtree = MockTree(name)
                self.value[name] = subtree
                subtree.lset([], val)
        else:
            self.value = value

    def llist(self, path):
        if path: return self._forward("llist", path)
        if self.is_leaf:
            return []
        else:
            return sorted(self.value.keys())

    def lcreate(self, path, value=None):
        if not path: return super(MockTree, self).lcreate(path, value)

        if self.is_leaf:
688
            subtree = MockTree(path[0])
Enrico Zini's avatar
Enrico Zini committed
689
690
            self.value = { path[0]: subtree }
        else:
691
692
693
694
            subtree = self.value.get(path[0], None)
            if subtree is None:
                subtree = MockTree(path[0])
                self.value[path[0]] = subtree
Enrico Zini's avatar
Enrico Zini committed
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710

        if len(path) == 1:
            subtree.lset([], value)
        else:
            subtree.lcreate(path[1:], value)

    def ldelete(self, path):
        if not path: return super(MockTree, self).ldelete(path)
        if self.is_leaf: return super(MockTree, self).ldelete(path)
        if len(path) == 1:
            del self.value[path[0]]
        else:
            return self._forward("ldelete", path)

    def ldoc(self, path):
        return None