class set_oper_dicttree_keys(Aspect): # the second dicttree is passed in as on on = aspect.config(on=None) # op is one of ("union", "intersection", "difference", "symmetric_difference") op = aspect.config(op="union") @aspect.plumb def __getitem__(_next, self, key): # whether keys appear is only handled by keys() itself if key not in self.keys(): raise KeyError(key) try: val_a = _next(key) if isinstance(val_a, dict) \ and key in self.on: if isinstance(self.on[key], dict): # values from A and B are branches, thus return # the next recursion level return set_oper_dicttree_keys(val_a, on=self.on[key], op=self.op) else: # value from A is a branch and B is a leaf, not coverable raise TypeError("Element for %s is not of type dictionary" % key) else: # Under the key, A has just a value # or (A has a branch and B hasn't anything) return val_a except KeyError: # if the key is not in A, pass # return the value from B, or raise KeyError return self.on[key] def iterkeys(self): # generator doesn't make sense, we need the full list for set # operations return iter(self.keys()) @aspect.plumb def keys(_next, self): '''Returns a list of keys resulting from the set operation op on the keys-lists of the two dictionaries''' keys_a = set(_next()) keys_b = set(self.on.keys()) return list(getattr(keys_a, self.op)(keys_b))
class hook(aspect.Aspect): '''Hook aspect fires events on __setitem__ in a dicttree. As event data the branch is passed to the matching callback methods. Config Keywords: specs -- list of specs, like (<path>, <value>, <event-type>) where path -- path to leaf in the dicttree value -- expected value, triggers event event-type -- triggered event Usage example @tpv.event.hook([ ('/approval', 'granted', 'user-approved'), ('/foo/bar/approval', 'granted', 'user-approved'), ('/foo/baz/approval', 'denied', 'user-denied') ]) class NotifyingDict(OrderedDict): pass ''' specs = aspect.config(None) # to be able to give translated specs to children, we internally # prefer a so called unrolled format, which is generated below by # unroll_specs. For the usage example above, it would look like # # unrolled_specs = \ # dict(approval=('granted', 'user-approved'), # foo =[ ('/bar/approval', 'granted', 'user-approved'), # ('/baz/approval', 'denied', 'user-denied') ]) @aspect.plumb def __init__(_next, self, *args, **kw): self.unrolled_specs = dict() \ if self.specs is None \ else unroll_specs(self.specs) _next(*args, **kw) @aspect.plumb def __getitem__(_next, self, key): val = _next(key) # if our spec wants to listen to a branch, we have to wrap # this branch in the hook as well, with the translated spec if isinstance(self.unrolled_specs.get(key, None), list): if not isinstance(val, dict): raise TypeError("hook aspect expected %s to be of type dict" % val) return hook(val, specs=self.unrolled_specs[key]) return val @aspect.plumb def __setitem__(_next, self, key, val): _next(key, val) try: value, evtype = self.unrolled_specs.get(key, (None, None)) if value == val: # a leaf has been set to an expected value, thus notify. # pass self (the current branch) as evdata. might be # useful in the callback. notify(evtype, self) except ValueError: # user must have set the value of a branch, that we have # been listening on. we'll keep on listening, anyway. pass
class set_oper_dicttree_items(Aspect): # the second dicttree is passed in as on on = aspect.config(on=None) # op is one of ("union", "intersection", "difference", "symmetric_difference") op = aspect.config(op="union") @aspect.plumb def __getitem__(_next, self, key, thorough=True): # check, whether the key remains after the set operation on # the keys. but don't do this, when called from iterkeys, else # we land in infinite recursion. if thorough and key not in self.keys(thorough=False): raise KeyError(key) # values for A and B with fallbacks try: val_a = _next(key) key_is_in_a = True except KeyError: # provide an empty fallback for value from A and record # we had to, to cover the case where B is a branch and A # is empty. val_a = [] key_is_in_a = False val_b = self.on.get(key, []) # check for branches if isinstance(val_a, dict): if isinstance(val_b, dict): # both are branches, do the recursion return set_oper_dicttree_items(val_a, on=val_b, op=self.op) elif key not in self.on: # we know, that self.op != 'intersection' at this # point (because then key is not in self.keys()). for # all other operations, it's fine to return the branch # from A return val_a else: # A is branch, B is leaf: misaligned case raise TypeError("Element for %s is not of type dictionary" % key) elif isinstance(val_b, dict): if not key_is_in_a: # B is a branch, A is empty # as the key was accepted, by the iterkeys() function, # self.op is in ("union", "symmetric_difference") and # thus return the sole branch from B return val_b else: # B is a branch, A is leaf: misaligned case raise TypeError("Element for %s is not of type dictionary" % key) # set operations on values ret = getattr(ensure_set(val_a), self.op)(ensure_set(val_b)) if len(ret) == 0: # nothing remains as value, thus KeyError raise KeyError(key) elif len(ret) == 1: # just one value remains, cast to scalar value return ret.pop() else: # convert set to list return list(ret) def __contains__(self, key, thorough=True): # check, whether any values remain after the set # operations. For thorough==True, check set operations for # values and keys. For thorough==False, check only set # operation on values. try: self.__getitem__(key, thorough=thorough) return True except KeyError: return False def keys(self, thorough=True): return list(self.iterkeys(thorough)) @aspect.plumb def iterkeys(_next, self, thorough=True): # 1. apply the set operation on the two key lists keys_a = set(_next()) keys_b = set(self.on.keys()) keys = getattr(keys_a, self.op)(keys_b) # thorough is used to break the dependency cycle between # __getitem__ and keys. if not thorough: # if called from __getitem__, we don't want to recurse # into __getitem again return keys else: # 2. a key might still not be in the keys of the result, # if the set operation on the values turns out empty return (key for key in keys if self.__contains__(key, thorough=False))
class cache(Aspect): # allow to supply a dictionary to be used as the cach'ing object # initialised as an empty dictionary by the constructor if None. cache = aspect.config(cache=None) # class of which the instances are to be cached node_class = aspect.config(node_class=dict) def _get_cache(self, node): '''Returns a cache object for a given node''' return getattr(node, "cache_class", self.cache.__class__ if self.cache is not None else dict)() def _wrap_child(self, child): return cache(child, cache=self._get_cache(child)) @aspect.plumb def __init__(_next, self, *args, **kwargs): if self.cache is None: self.cache = self._get_cache(self) # if not None, contains the whole list of keys (so that # repetitive calls to keys() are cached) self.cache_keys = None # marks, whether self.cache contains the whole dictionary self.cache_complete = False _next(*args, **kwargs) @aspect.plumb def __getitem__(_next, self, key): # try first in cache try: return self.cache[key] except KeyError: pass # else from the source ret = _next(key) if isinstance(ret, self.node_class): # branch case, do the recursion ret = self._wrap_child(ret) # commit value to cache self.cache[key] = ret return ret @aspect.plumb def __setitem__(_next, self, key, val): # hand through _next(key, val) # update cache self.cache[key] = val # keep cache_keys up-to-date if self.cache_keys is not None and key not in self.cache_keys: self.cache_keys.append(key) @aspect.plumb def update(_next, self, E, **F): # hand through _next(E, **F) # update cache self.cache.update(E, **F) if self.cache_keys is not None: keys = set() if E: if hasattr(E, "keys"): keys.update(E.keys()) else: keys.update(v for k, v in E) keys.update(F.keys()) self.cache_keys += list(keys.difference(self.cache_keys)) @aspect.plumb def __iter__(_next, self): if self.cache_keys is None: # start assembling a candidate for self.cache_keys tmp_cache_keys = [] # iterate over keys from source for key in _next(): tmp_cache_keys.append(key) yield key # StopIteration received, thus tmp_cache_keys is complete self.cache_keys = tmp_cache_keys else: # cache_keys is complete, iterate it for key in iter(self.cache_keys): yield key iterkeys = __iter__ def itervalues(self): # implemented based on iteritems to be able to fill the cache for key, val in self.iteritems(): yield val @aspect.plumb def iteritems(_next, self): if self.cache_complete: # the cache is complete return directly from it. # mixing iterators and return fails, thus as a shallow # generator for x in self.cache.iteritems(): yield x else: for key, val in _next(): if isinstance(val, self.node_class): # branch case, do the recursion val = self._wrap_child(val) # update in cache self.cache[key] = val # pass out, as generator yield (key, val) # StopIteration was reached, the cache is now complete self.cache_complete = True # cache_keys are thus fully generated self.cache_keys = self.cache.keys() # the non-generator functions, are overwritten to use the # generator ones (they use the cache) def items(self): return list(self.iteritems()) def keys(self): return list(self.iterkeys()) def values(self): return list(self.itervalues()) # provide a function to empty the cache, when one uses # side-channels to change the source def clear_cache(self): self.cache = self._get_cache(self) self.cache_complete = False self.cache_keys = []
class set_oper_dicttree_values(Aspect): # the second dicttree is passed in as on on = aspect.config(on=None) # op is one of ("union", "intersection", "difference", "symmetric_difference") op = aspect.config(op="union") @aspect.plumb def __getitem__(_next, self, key): # get the values from the two dictionaries # if the key is not in A, it's not meant to be in the result, # thus allow A to raise KeyError, but not B val_a = _next(key) val_b = self.on.get(key, []) # check for branches if isinstance(val_a, dict): if isinstance(val_b, dict): # both are branches, do the recursion return set_oper_dicttree_values(val_a, on=val_b, op=self.op) elif key not in self.on: # A is branch, B is empty if self.op == 'intersection': # for an intersection, key has to exist in B raise KeyError(key) else: # for union, (symmetric_)difference the branch # from A remains return val_a else: # A is branch, B has a leaf: misaligned case raise TypeError("Element for %s is not of type dictionary" % key) # apply the set operation on the values ret = getattr(ensure_set(val_a), self.op)(ensure_set(val_b)) if len(ret) == 0: # nothing remains as value, thus KeyError raise KeyError(key) elif len(ret) == 1: # just one value remains, cast to scalar value return ret.pop() else: # convert set to list return list(ret) def __contains__(self, key): # as we don't want to list a key, for which no value remains # after the set operation, we have to use __getitem__ try: self.__getitem__(key) return True except KeyError: return False def keys(self): return list(self.iterkeys()) @aspect.plumb def iterkeys(_next, self): # use the __contains__ function to filter available keys return (key for key in _next() if key in self)