diff options
Diffstat (limited to 'reconfigure/items/bound.py')
-rw-r--r-- | reconfigure/items/bound.py | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/reconfigure/items/bound.py b/reconfigure/items/bound.py new file mode 100644 index 0000000..f096965 --- /dev/null +++ b/reconfigure/items/bound.py @@ -0,0 +1,302 @@ +import json + + +class BoundCollection (object): + """ + Binds a list-like object to a set of nodes + + :param node: target node (its children will be bound) + :param item_class: :class:`BoundData` class for items + :param selector: ``lambda x: bool``, used to filter out a subset of nodes + """ + + def __init__(self, node, item_class, selector=lambda x: True): + self.node = node + self.selector = selector + self.item_class = item_class + self.data = [] + self.rebuild() + + def rebuild(self): + """ + Discards cached collection and rebuilds it from the nodes + """ + del self.data[:] + for node in self.node.children: + if self.selector(node): + self.data.append(self.item_class(node)) + + def to_dict(self): + return [x.to_dict() if hasattr(x, 'to_dict') else x for x in self] + + def to_json(self): + return json.dumps(self.to_dict(), indent=4) + + def __str__(self): + return self.to_json() + + def __iter__(self): + return self.data.__iter__() + + def __getitem__(self, index): + return self.data.__getitem__(index) + + def __len__(self): + return len(self.data) + + def __contains__(self, item): + return item in self.data + + def append(self, item): + self.node.append(item._node) + self.data.append(item) + + def remove(self, item): + self.node.remove(item._node) + self.data.remove(item) + + def insert(self, index, item): + self.node.children.insert(index, item._node) + self.data.insert(index, item) + + def pop(self, index): + d = self[index] + self.remove(d) + return d + + +class BoundDictionary (BoundCollection): + """ + Binds a dict-like object to a set of nodes. Accepts same params as :class:`BoundCollection` plus ``key`` + + :param key: ``lambda value: object``, is used to get key for value in the collection + """ + + def __init__(self, key=None, **kwargs): + self.key = key + BoundCollection.__init__(self, **kwargs) + + def rebuild(self): + BoundCollection.rebuild(self) + self.rebuild_dict() + + def rebuild_dict(self): + self.datadict = dict((self.key(x), x) for x in self.data) + + def to_dict(self): + return dict((k, x.to_dict() if hasattr(x, 'to_dict') else x) for k, x in self.iteritems()) + + def __getitem__(self, key): + self.rebuild_dict() + return self.datadict[key] + + def __setitem__(self, key, value): + self.rebuild_dict() + if not key in self: + self.append(value) + self.datadict[key] = value + + def __contains__(self, key): + self.rebuild_dict() + return key in self.datadict + + def __iter__(self): + self.rebuild_dict() + return self.datadict.__iter__() + + def iteritems(self): + return self.datadict.iteritems() + + def setdefault(self, k, v): + if not k in self: + self[k] = v + self.append(v) + return self[k] + + def values(self): + return self.data + + def update(self, other): + for k, v in other.iteritems(): + self[k] = v + + def pop(self, key): + if key in self: + self.remove(self[key]) + + +class BoundData (object): + """ + Binds itself to a node. + + ``bind_*`` classmethods should be called on module-level, after subclass declaration. + + :param node: all bindings will be relative to this node + :param kwargs: if ``node`` is ``None``, ``template(**kwargs)`` will be used to create node tree fragment + """ + + def __init__(self, node=None, **kwargs): + if not node: + node = self.template(**kwargs) + self._node = node + + def template(self, **kwargs): + """ + Override to create empty objects. + + :returns: a :class:`reconfigure.nodes.Node` tree that will be used as a template for new BoundData instance + """ + return None + + def to_dict(self): + res_dict = {} + for attr_key in self.__class__.__dict__: + if attr_key in self.__class__._bound: + attr_value = getattr(self, attr_key) + if isinstance(attr_value, BoundData): + res_dict[attr_key] = attr_value.to_dict() + elif isinstance(attr_value, BoundCollection): + res_dict[attr_key] = attr_value.to_dict() + else: + res_dict[attr_key] = attr_value + return res_dict + + def to_json(self): + return json.dumps(self.to_dict(), indent=4) + + def __str__(self): + return self.to_json() + + @classmethod + def bind(cls, data_property, getter, setter): + """ + Creates an arbitrary named property in the class with given getter and setter. Not usually used directly. + + :param data_property: property name + :param getter: ``lambda: object``, property getter + :param setter: ``lambda value: None``, property setter + """ + if not hasattr(cls, '_bound'): + cls._bound = [] + cls._bound.append(data_property) + setattr(cls, data_property, property(getter, setter)) + + @classmethod + def bind_property(cls, node_property, data_property, default=None, \ + default_remove=[], \ + path=lambda x: x, getter=lambda x: x, setter=lambda x: x): + """ + Binds the value of a child :class:`reconfigure.node.PropertyNode` to a property + + :param node_property: ``PropertyNode``'s ``name`` + :param data_property: property name to be created + :param default: default value of the property (is ``PropertyNode`` doesn't exist) + :param default_remove: if setting a value contained in default_remove, the target property is removed + :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. + :param getter: ``lambda object: object``, used to transform value when getting + :param setter: ``lambda object: object``, used to transform value when setting + """ + def pget(self): + prop = path(self._node).get(node_property) + if prop: + return getter(prop.value) + else: + return default + + def pset(self, value): + if setter(value) in default_remove: + node = path(self._node).get(node_property) + if node: + path(self._node).remove(node) + else: + path(self._node).set_property(node_property, setter(value)) + + cls.bind(data_property, pget, pset) + + @classmethod + def bind_attribute(cls, node_attribute, data_property, default=None, \ + path=lambda x: x, getter=lambda x: x, setter=lambda x: x): + """ + Binds the value of node object's attribute to a property + + :param node_attribute: ``Node``'s attribute name + :param data_property: property name to be created + :param default: default value of the property (is ``PropertyNode`` doesn't exist) + :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. + :param getter: ``lambda object: object``, used to transform value when getting + :param setter: ``lambda object: object``, used to transform value when setting + """ + def pget(self): + prop = getattr(path(self._node), node_attribute) + if prop: + return getter(prop) + else: + return getter(default) + + def pset(self, value): + setattr(path(self._node), node_attribute, setter(value)) + + cls.bind(data_property, pget, pset) + + @classmethod + def bind_collection(cls, data_property, path=lambda x: x, selector=lambda x: True, item_class=None, \ + collection_class=BoundCollection, **kwargs): + """ + Binds the subset of node's children to a collection property + + :param data_property: property name to be created + :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. + :param selector: ``lambda Node: bool``, can be used to filter out a subset of child nodes + :param item_class: a :class:`BoundData` subclass to be used for collection items + :param collection_class: a :class:`BoundCollection` subclass to be used for collection property itself + """ + def pget(self): + if not hasattr(self, '__' + data_property): + setattr(self, '__' + data_property, + collection_class( + node=path(self._node), + item_class=item_class, + selector=selector, + **kwargs + ) + ) + return getattr(self, '__' + data_property) + + cls.bind(data_property, pget, None) + + @classmethod + def bind_name(cls, data_property, getter=lambda x: x, setter=lambda x: x): + """ + Binds the value of node's ``name`` attribute to a property + + :param data_property: property name to be created + :param getter: ``lambda object: object``, used to transform value when getting + :param setter: ``lambda object: object``, used to transform value when setting + """ + def pget(self): + return getter(self._node.name) + + def pset(self, value): + self._node.name = setter(value) + + cls.bind(data_property, pget, pset) + + @classmethod + def bind_child(cls, data_property, path=lambda x: x, item_class=None): + """ + Directly binds a child Node to a BoundData property + + :param data_property: property name to be created + :param path: ``lambda self.node: PropertyNode``, can be used to point binding to another Node instead of ``self.node``. + :param item_class: a :class:`BoundData` subclass to be used for the property value + """ + def pget(self): + if not hasattr(self, '__' + data_property): + setattr(self, '__' + data_property, + item_class( + path(self._node), + ) + ) + return getattr(self, '__' + data_property) + + cls.bind(data_property, pget, None) |