From 06e95fc542284e69d8c7cc9bdf845f296526d97c 2009-08-10 20:42:57
From: Brian Granger <ellisonbg@gmail.com>
Date: 2009-08-10 20:42:57
Subject: [PATCH] Improvement to how config is handled in Components.

When a Component gets a config object, it aways makes a deepcopy of it.

---

diff --git a/IPython/core/component.py b/IPython/core/component.py
index d86e60f..853f4d2 100644
--- a/IPython/core/component.py
+++ b/IPython/core/component.py
@@ -20,7 +20,7 @@ Authors:
 # Imports
 #-----------------------------------------------------------------------------
 
-
+from copy import deepcopy
 from weakref import WeakValueDictionary
 
 from IPython.utils.ipstruct import Struct
@@ -134,7 +134,7 @@ class Component(HasTraitlets):
         name : str
             The unique name of the component.  If empty, then a unique
             one will be autogenerated.
-        config : Config
+        config : Struct
             If this is empty, self.config = parent.config, otherwise
             self.config = config and root.config is ignored.  This argument
             should only be used to *override* the automatic inheritance of 
@@ -165,10 +165,10 @@ class Component(HasTraitlets):
         self.root = self # This is the default, it is set when parent is set
         self.parent = parent
         if config is not None:
-            self.config = config
+            self.config = deepcopy(config)
         else:
             if self.parent is not None:
-                self.config = self.parent.config
+                self.config = deepcopy(self.parent.config)
 
     #-------------------------------------------------------------------------
     # Static traitlet notifiations
@@ -193,7 +193,18 @@ class Component(HasTraitlets):
             if not self.parent.root is new:
                 raise ComponentError("Error in setting the root attribute: "
                                      "root != parent.root")
-        
+
+    def _config_changed(self, name, old, new):
+        # Get all traitlets with a config_key metadata entry
+        traitlets = self.traitlets(config_key=lambda v: True)
+        for k, v in traitlets.items():
+            try:
+                config_value = new[v.get_metadata('config_key')]
+            except KeyError:
+                pass
+            else:
+                setattr(self, k, config_value)
+
     @property
     def children(self):
         """A list of all my child components."""
diff --git a/IPython/core/tests/test_component.py b/IPython/core/tests/test_component.py
index ebcb61a..bc21616 100644
--- a/IPython/core/tests/test_component.py
+++ b/IPython/core/tests/test_component.py
@@ -24,7 +24,7 @@ from unittest import TestCase
 
 from IPython.core.component import Component, ComponentError
 from IPython.utils.traitlets import (
-    TraitletError
+    TraitletError, Int, Float, Str
 )
 from IPython.utils.ipstruct import Struct
 
@@ -147,6 +147,30 @@ class TestComponentConfig(TestCase):
         self.assertEquals(c1.config, config)
         self.assertEquals(c2.config, config)
         self.assertEquals(c3.config, config)
+        # Test that we always make copies
+        self.assert_(c1.config is not config)
+        self.assert_(c2.config is not config)
+        self.assert_(c3.config is not config)
+        self.assert_(c1.config is not c2.config)
+        self.assert_(c2.config is not c3.config)
+        
+    def test_inheritance(self):
+        class MyComponent(Component):
+            a = Int(1, config_key='A')
+            b = Float(1.0, config_key='B')
+            c = Str('no config')
+        config = Struct()
+        config.A = 2
+        config.B = 2.0
+        c1 = MyComponent(None, config=config)
+        c2 = MyComponent(c1)
+        self.assertEquals(c1.a, config.A)
+        self.assertEquals(c1.b, config.B)
+        self.assertEquals(c2.a, config.A)
+        self.assertEquals(c2.b, config.B)
+        c4 = MyComponent(c2, config=Struct())
+        self.assertEquals(c4.a, 1)
+        self.assertEquals(c4.b, 1.0)
 
 class TestComponentName(TestCase):
 
diff --git a/IPython/utils/ipstruct.py b/IPython/utils/ipstruct.py
index 1f620cf..6816295 100644
--- a/IPython/utils/ipstruct.py
+++ b/IPython/utils/ipstruct.py
@@ -42,7 +42,7 @@ class Struct(dict):
     * Intelligent merging.
     * Overloaded operators.
     """
-
+    _allownew = True
     def __init__(self, *args, **kw):
         """Initialize with a dictionary, another Struct, or data.
 
diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py
index d51f5ee..4edaab6 100644
--- a/IPython/utils/tests/test_traitlets.py
+++ b/IPython/utils/tests/test_traitlets.py
@@ -327,15 +327,37 @@ class TestHasTraitletsNotify(TestCase):
         self.assertEquals(len(a._traitlet_notifiers['a']),0)
 
 
-class TestTraitletKeys(TestCase):
+class TestHasTraitlets(TestCase):
 
-    def test_keys(self):
+    def test_traitlet_names(self):
         class A(HasTraitlets):
-            a = Int
-            b = Float
+            i = Int
+            f = Float
+        a = A()
+        self.assertEquals(a.traitlet_names(),['i','f'])
+
+    def test_traitlet_metadata(self):
+        class A(HasTraitlets):
+            i = Int(config_key='MY_VALUE')
         a = A()
-        self.assertEquals(a.traitlet_names(),['a','b'])
+        self.assertEquals(a.traitlet_metadata('i','config_key'), 'MY_VALUE')
 
+    def test_traitlets(self):
+        class A(HasTraitlets):
+            i = Int
+            f = Float
+        a = A()
+        self.assertEquals(a.traitlets(), dict(i=A.i, f=A.f))
+
+    def test_traitlets_metadata(self):
+        class A(HasTraitlets):
+            i = Int(config_key='VALUE1', other_thing='VALUE2')
+            f = Float(config_key='VALUE3', other_thing='VALUE2')
+        a = A()
+        # traitlets = a.traitlets(config_key=lambda v: True)
+        # self.assertEquals(traitlets, dict(i=A.i, f=A.f))
+        traitlets = a.traitlets(config_key='VALUE1', other_thing='VALUE2')
+        self.assertEquals(traitlets, dict(i=A.i))
 
 #-----------------------------------------------------------------------------
 # Tests for specific traitlet types
diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py
index f83ea9f..8667809 100644
--- a/IPython/utils/traitlets.py
+++ b/IPython/utils/traitlets.py
@@ -52,7 +52,7 @@ Authors:
 import inspect
 import sys
 import types
-from types import InstanceType, ClassType
+from types import InstanceType, ClassType, FunctionType
 
 ClassTypes = (ClassType, type)
 
@@ -133,6 +133,18 @@ def parse_notifier_name(name):
             assert isinstance(n, str), "names must be strings"
         return name
 
+
+class _SimpleTest:
+    def __init__ ( self, value ): self.value = value
+    def __call__ ( self, test  ):
+        print test, self.value 
+        return test == self.value
+    def __repr__(self):
+        return "<SimpleTest(%r)" % self.value
+    def __str__(self):
+        return self.__repr__()
+
+
 #-----------------------------------------------------------------------------
 # Base TraitletType for all traitlets
 #-----------------------------------------------------------------------------
@@ -166,7 +178,16 @@ class TraitletType(object):
         """
         if default_value is not NoDefaultSpecified:
             self.default_value = default_value
-        self.metadata.update(metadata)
+
+        if len(metadata) > 0:
+            if len(self.metadata) > 0:
+                self._metadata = self.metadata.copy()
+                self._metadata.update(metadata)
+            else:
+                self._metadata = metadata
+        else:
+            self._metadata = self.metadata
+
         self.init()
 
     def init(self):
@@ -238,6 +259,12 @@ class TraitletType(object):
                 % (self.name, self.info(), repr_type(value))            
         raise TraitletError(e)
 
+    def get_metadata(self, key):
+        return getattr(self, '_metadata', {}).get(key, None)
+
+    def set_metadata(self, key, value):
+        getattr(self, '_metadata', {})[key] = value
+
 
 #-----------------------------------------------------------------------------
 # The HasTraitlets implementation
@@ -303,7 +330,6 @@ class HasTraitlets(object):
         for key in dir(cls):
             value = getattr(cls, key)
             if isinstance(value, TraitletType):
-                # print 'value: ', value
                 value.set_default_value(inst)
         return inst
 
@@ -408,10 +434,44 @@ class HasTraitlets(object):
             for n in names:
                 self._add_notifiers(handler, n)
 
-    def traitlet_names(self):
+    def traitlet_names(self, **metadata):
         """Get a list of all the names of this classes traitlets."""
-        return [memb[0] for memb in inspect.getmembers(self.__class__) if isinstance(memb[1], TraitletType)]
+        return self.traitlets(**metadata).keys()
 
+    def traitlets(self, **metadata):
+        """Get a list of all the traitlets of this class.
+
+        The TraitletTypes returned don't know anything about the values
+        that the various HasTraitlet's instances are holding.
+        """
+        traitlets = dict([memb for memb in inspect.getmembers(self.__class__) if \
+                     isinstance(memb[1], TraitletType)])
+        if len(metadata) == 0:
+            return traitlets
+
+        for meta_name, meta_eval in metadata.items():
+            if type(meta_eval) is not FunctionType:
+                metadata[meta_name] = _SimpleTest(meta_eval)
+
+        result = {}
+        for name, traitlet in traitlets.items():
+            for meta_name, meta_eval in metadata.items():
+                if not meta_eval(traitlet.get_metadata(meta_name)):
+                    break
+            else:
+                result[name] = traitlet
+
+        return result
+
+    def traitlet_metadata(self, traitletname, key):
+        """Get metadata values for traitlet by key."""
+        try:
+            traitlet = getattr(self.__class__, traitletname)
+        except AttributeError:
+            raise TraitletError("Class %s does not have a traitlet named %s" %
+                                (self.__class__.__name__, traitletname))
+        else:
+            return traitlet.get_metadata(key)
 
 #-----------------------------------------------------------------------------
 # Actual TraitletTypes implementations/subclasses