changeset 1345:e0400a2b7c35

Use trait instead of mixin for component_manager
author Thinker K.F. Li <thinker@codemud.net>
date Fri, 11 Feb 2011 15:10:37 +0800
parents 8f1f8ef5c9ea
children 4fdc998d3dc3
files pyink/domview.py pyink/trait.py
diffstat 2 files changed, 351 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/pyink/domview.py	Wed Feb 09 07:07:08 2011 +0800
+++ b/pyink/domview.py	Fri Feb 11 15:10:37 2011 +0800
@@ -1,6 +1,7 @@
 import random
 import dom_event
 from tween import TweenObject
+from trait import trait, require, composite
 
 
 ## \brief Compare two nodes with ID.
@@ -48,8 +49,8 @@
     #
     # \param comp_node is None for main component.
     #
-    def __init__(self, domview, comp_node):
-        self._domview = domview
+    def __init__(self, comp_mgr, comp_node):
+        self._comp_mgr = comp_mgr
         self.node = comp_node
         self.layers = []
         self.timelines = []
@@ -66,11 +67,11 @@
                 break
             pass
         else:                   # no any ns0:scenes
-            doc = self._domview._doc
+            doc = self._comp_mgr._doc
             scenes_node = doc.createElement('ns0:scenes')
             scenes_node.setAttribute('name', 'default')
             
-            node_id = self._domview.new_id()
+            node_id = self._comp_mgr.new_id()
             scenes_node.setAttribute('id', node_id)
             
             comp_node.appendChild(scenes_node)
@@ -126,12 +127,12 @@
             raise ValueError, \
                 'try add a timeline with duplicated name - %s' % (name)
         
-        doc = self._domview._doc
+        doc = self._comp_mgr._doc
         comp_node = self.node
         
         scenes_node = doc.createElement('ns0:scenes')
         scenes_node.setAttribute('name', name)
-        node_id = self._domview.new_id()
+        node_id = self._comp_mgr.new_id()
         scenes_node.setAttribute('id', node_id)
 
         comp_node.appendChild(scenes_node)
@@ -219,7 +220,7 @@
     pass
 
 
-## \brief A mix-in for class domview for management of components.
+## \brief A trait for class domview for managing components.
 #
 # This class is responsible for manage components and timelines.  It
 # is also responsible for switching component and timeline.  Switching
@@ -235,8 +236,20 @@
 # special case with slightly different in structure.  It should be
 # removed and normalized to normal components.
 #
+@trait
 class component_manager(component_manager_ui_update):
-    def __init__(self, domview):
+    _layers = require
+    _scenes_node = require
+    _metadata_node = require
+    _doc = require
+    _root = require
+    _layers = require
+    _layers_parent = require
+    new_id = require
+    get_node = require
+    reset = require
+
+    def __init__(self):
         super(component_manager, self).__init__()
         self._components_node = None
         self._components = []
@@ -245,14 +258,12 @@
         self._cur_comp = None
         self._cur_timeline = None
         self._components_group = None
-        
-        self._domview = domview
         pass
 
     def _set_main_component(self):
-        comp = Component(self._domview, None)
-        comp.layers = self._domview._layers
-        scenes_node = self._domview._scenes_node
+        comp = Component(self, None)
+        comp.layers = self._layers
+        scenes_node = self._scenes_node
         timeline = Timeline(scenes_node)
         comp.timelines = [timeline]
 
@@ -272,7 +283,7 @@
             if child_name in comp_names:
                 raise ValueError, 'duplicate component name %s' % (child_name)
 
-            comp = Component(self._domview, child)
+            comp = Component(self, child)
             comp.parse_timelines()
             
             self._components.append(comp)
@@ -285,7 +296,7 @@
     # This method is called by domview._init_metadata().
     #
     def _component_manager_init_metadata(self):
-        metadata_node = self._domview._metadata_node
+        metadata_node = self._metadata_node
 
         # Make sure ns0:components in metadata
 	for n in metadata_node.childList():
@@ -295,13 +306,13 @@
 	    pass
 	else:
 	    components_node = \
-                self._domview._doc.createElement("ns0:components")
+                self._doc.createElement("ns0:components")
 	    metadata_node.appendChild(components_node)
 	    self._components_node = components_node
 	    pass
         
         # Make sure the group for containing components.
-        for n in self._domview._root.childList():
+        for n in self._root.childList():
             if n.name() != 'svg:g':
                 continue
             try:
@@ -313,12 +324,12 @@
                 break
             pass
         else:                   # no components group
-            components_group = self._domview._doc.createElement('svg:g')
+            components_group = self._doc.createElement('svg:g')
             components_group.setAttribute('inkscape:label', 'components')
-            gid = self._domview.new_id()
+            gid = self.new_id()
             components_group.setAttribute('id', gid)
             
-            self._domview._root.appendChild(components_group)
+            self._root.appendChild(components_group)
             self._components_group = components_group
             pass
         pass
@@ -331,7 +342,7 @@
         self._cur_comp = self._main_comp
         tl = self._main_comp.get_timeline('default')
         self._cur_timeline = tl
-        self._domview._scenes_node = tl.scenes_node
+        self._scenes_node = tl.scenes_node
         pass
 
     ## \brief Create component group
@@ -340,16 +351,16 @@
     # The layers group is where layer groups is in.
     #
     def _create_component_group(self):
-        doc = self._domview._doc
+        doc = self._doc
         group = doc.createElement('svg:g')
-        gid = self._domview.new_id()
+        gid = self.new_id()
         group.setAttribute('id', gid)
         
         self._components_group.appendChild(group)
 
         # Create layers group
         layers_group = doc.createElement('svg:g')
-        gid = self._domview.new_id()
+        gid = self.new_id()
         layers_group.setAttribute('id', gid)
         layers_group.setAttribute('inkscape:label', 'layers')
         group.appendChild(layers_group)
@@ -363,8 +374,8 @@
     # \return a ns0:component node.
     #
     def _create_component_node(self, comp_name, comp_group_id):
-        comp_node = self._domview._doc.createElement('ns0:component')
-        comp_id = self._domview.new_id()
+        comp_node = self._doc.createElement('ns0:component')
+        comp_id = self.new_id()
         comp_node.setAttribute('id', comp_id)
         comp_node.setAttribute('name', comp_name)
         comp_node.setAttribute('ref', comp_group_id)
@@ -385,8 +396,8 @@
     ## \brief Create a layer group for give layer of a component.
     #
     def _create_comp_layer_group(self, layers_group, layer_name):
-        doc = self._domview._doc
-        gid = self._domview.new_id()
+        doc = self._doc
+        gid = self.new_id()
         
         layer_group = doc.createElement('svg:g')
         layer_group.setAttribute('id', gid)
@@ -408,7 +419,7 @@
 
     def _get_layers_group_of_component(self, comp_name):
         if comp_name == 'main':
-            return self._domview._root
+            return self._root
         
         comp_group = self.get_component_group(comp_name)
         layers_group = comp_group.firstChild()
@@ -453,10 +464,10 @@
         
         comp = self._get_component(comp_name)
         self._cur_comp = comp
-        self._domview._layers = comp.layers
+        self._layers = comp.layers
         comp_name = self._cur_comp.name()
         # for domview
-        self._domview._layers_parent = \
+        self._layers_parent = \
             self._get_layers_group_of_component(comp_name)
         
         first_name = comp.all_timeline_names()[0]
@@ -475,7 +486,7 @@
         comp_group_id = comp_group.getAttribute('id')
         comp_node = self._create_component_node(comp_name, comp_group_id)
         
-        comp = Component(self._domview, comp_node)
+        comp = Component(self, comp_node)
         comp.parse_timelines()
 
         self._components.append(comp)
@@ -491,7 +502,7 @@
         pass
 
     def add_component_node(self, comp_node):
-        comp = Component(self._domview, comp_node)
+        comp = Component(self, comp_node)
         comp_name = comp.name()
         if self.has_component(comp_name):
             raise ValueError, \
@@ -523,11 +534,11 @@
         
         comp_name = comp.name()
         if comp_name == 'main':
-            return self._domview._root
+            return self._root
         
         comp_node = comp.node
         gid = comp_node.getAttribute('ref')
-        comp_group = self._domview.get_node(gid)
+        comp_group = self.get_node(gid)
         return comp_group
 
     def get_current_component(self):
@@ -544,7 +555,7 @@
             if child.name() != 'ns0:scene':
                 continue
             gid = child.getAttribute('ref')
-            group = self._domview.get_node(gid)
+            group = self.get_node(gid)
             group.setAttribute('style', 'display: none')
             pass
         pass
@@ -556,10 +567,10 @@
         
         tl = self._cur_comp.get_timeline(timeline_name)
         self._cur_timeline = tl
-        self._domview._scenes_node = tl.scenes_node # of class domview
+        self._scenes_node = tl.scenes_node # of class domview
 
         # Make domview to rescan layers and scenes.
-        self._domview.reset()            # from domview
+        self.reset()            # from domview
         pass
 
     def add_timeline(self, timeline_name):
@@ -614,10 +625,10 @@
     def link_to_component(self, comp_name, parent_group):
         layers_group = self._get_layers_group_of_component(comp_name)
         
-        use_node = self._domview._doc.createElement('svg:use')
+        use_node = self._doc.createElement('svg:use')
         layers_group_id = layers_group.getAttribute('id')
         use_node.setAttribute('xlink:href', '#' + layers_group_id)
-        use_node_id = self._domview.new_id()
+        use_node_id = self.new_id()
         use_node.setAttribute('id', use_node_id)
         use_node.setAttribute('use_component', 'true')
 
@@ -962,7 +973,15 @@
 # change and destroy scene node and scene group.  A scene node is a 'ns0:scene'
 # in 'ns0:scenes' tag.  A scene group is respective 'svg:g' for a scene.
 #
+@composite
 class domview(domview_monitor):
+    use_traits = (component_manager,)
+    
+    method_map_component_manager = \
+        {component_manager._start_component_manager:
+             '_start_component_manager'}
+    method_map_traits = {component_manager: method_map_component_manager}
+
     # Declare variables, here, for keeping tracking
     _doc = None
     _root = None
@@ -977,15 +996,6 @@
         self._scenes_node = None
         self._layers_parent = None
         self._layers = []
-
-        self._comp_mgr = component_manager(self)
-        # Mixing-in component_manager to domview.
-        for attr in dir(self._comp_mgr):
-            if not attr.startswith('_'):
-                v = getattr(self._comp_mgr, attr)
-                setattr(self, attr, v)
-                pass
-            pass
 	pass
 
     ## \brief Create a scenes node if not existed.
@@ -1052,7 +1062,7 @@
 	
 	self._init_metadata()
 	self._start_monitor()	# from domview_monitor
-        self._comp_mgr._start_component_manager()
+        self._start_component_manager()
 	self._parse_all_layers()
 	pass
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pyink/trait.py	Fri Feb 11 15:10:37 2011 +0800
@@ -0,0 +1,291 @@
+## \brief Implement descriptors for mapping attributes for traits.
+#
+# The instances of require map attributes of traits to corresponding
+# attributes of the instance of the composition class.
+#
+class require(object):
+    def __get__(self, instance, owner):
+        if not instance:        # from a class object
+            return self
+        
+        attrname = instance._trait_attrname_map[self]
+        composite_obj = instance._trait_composite_obj
+        val = getattr(composite_obj, attrname)
+        return val
+
+    def __set__(self, instance, value):
+        attrname = instance._trait_attrname_map[self]
+        composite_obj = instance._trait_composite_obj
+        setattr(composite_obj, attrname, value)
+        pass
+    pass
+
+
+## \brief Decorator for making a class being a trait.
+#
+def trait(trait_clazz):
+    attrname_map = {}
+    trait_clazz._trait_attrname_map = attrname_map
+    
+    for attr in dir(trait_clazz):
+        value = getattr(trait_clazz, attr)
+        if value != require:
+            continue
+
+        require_o = require()
+        setattr(trait_clazz, attr, require_o)
+        attrname_map[require_o] = attr
+        pass
+
+    trait_clazz._is_trait = True
+    
+    return trait_clazz
+
+
+## \brief The function to return a proxy for a method of a trait.
+#
+def trait_method_proxy(trait_clazz, method):
+    def trait_method_proxy_real(self, *args, **kws):
+        if not hasattr(self, '_all_trait_objs'):
+            # self is an instance of the class composed from traits.
+            self._all_trait_objs = {}
+            pass
+        
+        try:
+            trait_obj = self._all_trait_objs[trait_clazz]
+        except KeyError:
+            trait_obj = trait_clazz()
+            trait_obj._trait_composite_obj = self
+            self._all_trait_objs[trait_clazz] = trait_obj
+            pass
+        
+        r = method(trait_obj, *args, **kws)
+        
+        return r
+    
+    return trait_method_proxy_real
+
+
+## \brief Derive and modify an existing trait.
+#
+def derive_trait(a_trait, composite_clazz):
+    attrname_map = None
+    if hasattr(composite_clazz, 'provide_traits'):
+        provide_traits = composite_clazz.provide_traits
+        if a_trait in provide_traits:
+            provide_trait = provide_traits[a_trait]
+            attrname_map = dict(a_trait._trait_attrname_map)
+            attrname_map.update(provide_trait)
+            pass
+        pass
+    
+    dic = {}
+    if attrname_map:
+        dic['_trait_attrname_map'] = attrname_map
+        pass
+    
+    derived = type('derived_trait', (a_trait,), dic)
+    
+    return derived
+
+## \brief A decorator to make class composited from traits.
+#
+# The class decorated by composite must own a use_traits attribute.
+#
+# \verbatim
+# @trait
+# class trait_a(object):
+#   var_a = require
+#   def xxx(self): return self.var_a
+#
+# @trait
+# class trait_b(object):
+#   def ooo(self): pass
+#
+# @composite
+# class foo(object):
+#    use_traits = (trait_a, trait_b)
+#
+#    var_a = 'value of var_a'
+#    pass
+#
+# obj = foo()
+# \endverbatim
+#
+# To make a class from a set of traits.  You must decorate the class
+# with the decorator 'composite'.  The class must has an attribute,
+# named use_traits, to provide a list or tuple of traits.
+#
+# Class that defines a trait must decorated with the decorator
+# 'trait'.  If the trait need to access state (varaibles) of the
+# intances of composition class, it must define attributes with value
+# 'require', likes what 'var_a' of trait_a does.  Then, the attributes
+# would be mapped to corresponding attributes of instances of
+# composition class.  For example, when you call obj.xxx(), it returns
+# value of 'var_a', and attribute 'var_a' is a property that returns
+# the value of 'var_a' of 'obj', an instance of class foo.
+#
+# By default, traits map attribute 'var_a' to 'var_a' of instances of
+# composition classes.  But, you can change it by specifying the map
+# in an attribute, named 'provide_traits', defined in composition
+# class.  The attribute provide_traits is a dictionary mapping from
+# trait class to a dictionary, named 'attrname_map' for the trait.
+# The attrname_map maps require attributes of the trait to names of
+# attributes of instances of the composition class.
+#
+def composite(clazz):
+    if not hasattr(clazz, 'use_traits'):
+        raise KeyError, \
+            '%s has no use_trait: it must be a list of traits' % (repr(clazz))
+    traits = clazz.use_traits
+    
+    for a_trait in traits:
+        if not hasattr(a_trait, '_is_trait'):
+            raise TypeError, '%s is not a trait' % (repr(a_trait))
+        pass
+
+    #
+    # Check content of clazz.provide_traits
+    #
+    if hasattr(clazz, 'provide_traits'):
+        if not isinstance(clazz.provide_traits, dict):
+            raise TypeError, \
+                'provide_traits of a composite must be a dictionary'
+        
+        provide_set = set(clazz.provide_traits.keys())
+        trait_set = set(traits)
+        unused_set = provide_set - trait_set
+        if unused_set:
+            raise ValueError, \
+                'can not find %s in provide_traits' % (repr(unused_set.pop()))
+
+        for trait, attrname_map in clazz.provide_traits.items():
+            for req in attrname_map:
+                if not isinstance(req, require):
+                    raise TypeError, \
+                        '%s is not a require: key of an ' \
+                        'attribute name map must be a require' % (repr(req))
+                pass
+            pass
+        pass
+    
+    #
+    # Count number of appearing in all traits for every attribute name.
+    #
+    attrname_cnts = {}
+    for a_trait in traits:
+        for attr in dir(a_trait):
+            if attr.startswith('_'):
+                continue
+            
+            value = getattr(a_trait, attr)
+            if value == require:
+                continue
+            
+            attrname_cnts[attr] = attrname_cnts.setdefault(attr, 0) + 1
+            pass
+        pass
+
+    if hasattr(clazz, 'method_map_traits'):
+        method_map_traits = clazz.method_map_traits
+    else:
+        method_map_traits = {}
+        pass
+    
+    #
+    # Set a proxy for every exported methods.
+    #
+    derived_traits = clazz._derived_traits = {}
+    for a_trait in traits:
+        derived = derive_trait(a_trait, clazz)
+        derived_traits[a_trait] = derived
+
+        if a_trait in method_map_traits:
+            method_map_trait = method_map_traits[a_trait]
+        else:
+            method_map_trait = {}
+        
+        for attr in dir(derived):
+            if attr not in attrname_cnts: # hidden
+                continue
+            if attrname_cnts[attr] > 1: # conflict
+                continue
+            
+            if hasattr(clazz, attr): # override
+                continue
+
+            value = getattr(a_trait, attr)
+            if value in method_map_trait: # do it later
+                continue
+            
+            value = getattr(derived, attr)
+            
+            if not callable(value):
+                raise TypeError, \
+                    '%s.%s is not a callable' % (repr(a_trait), attr)
+            
+            func = value.im_func
+            proxy = trait_method_proxy(derived, func)
+            setattr(clazz, attr, proxy)
+            pass
+        pass
+
+    #
+    # Map methods specified in method_map_traits.
+    #
+    for a_trait, method_map_trait in method_map_traits.items():
+        if a_trait not in derived_traits:
+            raise TypeError, \
+                '%s is not a trait used by the composition class' % \
+                (repr(a_trait))
+        
+        derived = derived_traits[a_trait]
+        for method, attrname in method_map_trait.items():
+            if not callable(method):
+                raise TypeError, \
+                    '%s.%s is not a callable' % (repr(a_trait), repr(method))
+            func = method.im_func
+            proxy = trait_method_proxy(derived, func)
+            setattr(clazz, attrname, proxy)
+            pass
+        pass
+    
+    return clazz
+
+
+if __name__ == '__main__':
+    @trait
+    class hello(object):
+        msg = require
+        
+        def hello(self, name):
+            return self.msg + ' hello ' + name
+        pass
+
+    @trait
+    class bye(object):
+        msg = require
+        
+        def bye(self, name):
+            return self.msg + ' bye ' + name
+        pass
+    
+    @composite
+    class hello_bye(object):
+        use_traits = (hello, bye)
+        
+        provide_hello = {hello.msg: 'msg1'}
+        provide_traits = {hello: provide_hello}
+        
+        method_map_hello = {hello.hello: 'hello1'}
+        method_map_traits = {hello: method_map_hello}
+
+        msg = 'hello_bye'
+        msg1 = 'hello_bye_msg1'
+        pass
+
+    o = hello_bye()
+    assert o.hello1('Miky') == 'hello_bye_msg1 hello Miky'
+    assert o.bye('Miky') == 'hello_bye bye Miky'
+    print 'OK'
+    pass