summaryrefslogtreecommitdiff
path: root/reconfigure/items/bound.py
diff options
context:
space:
mode:
Diffstat (limited to 'reconfigure/items/bound.py')
-rw-r--r--reconfigure/items/bound.py302
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)