From 7d974efa709ffe89fd053881960edc4daaed89c1 2009-08-06 22:19:06 From: Brian Granger Date: 2009-08-06 22:19:06 Subject: [PATCH] Improvements to component.py. Components are now based fully on traitlets. The have a better querying interface and smart children, parent, root properties. Some testing has been added. --- diff --git a/IPython/core/component.py b/IPython/core/component.py index 2be6db6..d2659d8 100644 --- a/IPython/core/component.py +++ b/IPython/core/component.py @@ -23,9 +23,9 @@ Authors: from weakref import WeakValueDictionary +from IPython.utils.ipstruct import Struct from IPython.utils.traitlets import ( - HasTraitlets, TraitletError, MetaHasTraitlets, - Int, Float, Str, Bool, Unicode + HasTraitlets, TraitletError, MetaHasTraitlets, Instance, This ) @@ -34,10 +34,6 @@ from IPython.utils.traitlets import ( #----------------------------------------------------------------------------- -class Config(object): - pass - - class MetaComponentTracker(type): """A metaclass that tracks instances of Components and its subclasses.""" @@ -47,9 +43,9 @@ class MetaComponentTracker(type): cls.__numcreated = 0 def __call__(cls, *args, **kw): - """Called when class is called (instantiated)!!! + """Called when *class* is called (instantiated)!!! - Then a Component or subclass is instantiated, this is called and + When a Component or subclass is instantiated, this is called and the instance is saved in a WeakValueDictionary for tracking. """ @@ -60,32 +56,34 @@ class MetaComponentTracker(type): c.__instance_refs[c.__numcreated] = instance return instance - def get_instances(cls): - """Get all instances of cls and its subclasses.""" - return cls.__instance_refs.values() + def get_instances(cls, name=None, klass=None, root=None): + """Get all instances of cls and its subclasses. - def get_instances_by_name(cls, name): - """Get all instances of cls and its subclasses by name.""" - return [i for i in cls.get_instances() if i.name == name] - - def get_instances_by_subclass(cls, thisclass): - """Get all instances of cls that are instances of thisclass. - - This includes all instances of subclasses of thisclass. + Parameters + ---------- + name : str + Limit to components with this name. + klass : class + Limit to components having isinstance(component, klass) + root : Component or subclass + Limit to components having this root. """ - return [i for i in cls.get_instances() if isinstance(i, thisclass)] - - def get_instances_by_class(cls, thisclass): - """Get all instances of cls that are instances of thisclass. - - This exclused instances of thisclass subclasses. + instances = cls.__instance_refs.values() + if name is not None: + instances = [i for i in instances if i.name == name] + if klass is not None: + instances = [i for i in instances if isinstance(i, klass)] + if root is not None: + instances = [i for i in instances if i.root == root] + return instances + + def get_instances_by_condition(cls, call, name=None, klass=None, root=None): + """Get all instances of cls, i such that call(i)==True. + + This also takes the ``name``, ``klass`` and ``root`` arguments of + :meth:`get_instance` """ - - return [i for i in cls.get_instances() if type(i) is thisclass] - - def get_instances_by_condition(cls, call): - """Get all instances of cls, i such that call(i)==True.""" - return [i for i in cls.get_instances() if call(i)] + return [i for i in cls.get_instances(name,klass,root) if call(i)] class ComponentNameGenerator(object): @@ -117,7 +115,10 @@ class Component(HasTraitlets): __metaclass__ = MetaComponent - config = Config() + # Traitlets are fun! + config = Instance(Struct) + parent = This(allow_none=True) + root = This(allow_none=True) def __init__(self, parent, name=None, config=None): """Create a component given a parent. @@ -140,11 +141,13 @@ class Component(HasTraitlets): to this argument. We might think about changing this behavior. """ super(Component, self).__init__() + self._children = [] if name is None: - self._name = ComponentNameGenerator() + self.name = ComponentNameGenerator() else: - self._name = name - self.parent = parent # this uses the property and handles None + self.name = name + 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 else: @@ -152,33 +155,35 @@ class Component(HasTraitlets): self.config = self.parent.config #------------------------------------------------------------------------- - # Properties + # Static traitlet notifiations #------------------------------------------------------------------------- - def _set_name(self, name): - # This should use the ComponentNameGenerator to test for uniqueness - self._name = name - - def _get_name(self): - return self._name + def _parent_changed(self, name, old, new): + if old is not None: + old._remove_child(self) + if new is not None: + new._add_child(self) - name = property(_get_name, _set_name) - - def _set_parent(self, parent): - if parent is None: - self._parent = None - self._root = self + if new is None: + self.root = self else: - assert isinstance(parent, Component), 'parent must be a component' - self._parent = parent - self._root = parent.root - - def _get_parent(self): - return self._parent - - parent = property(_get_parent, _set_parent) + self.root = new.root @property - def root(self): - return self._root - + def children(self): + """A list of all my child components.""" + return self._children + + def _remove_child(self, child): + """A private method for removing children componenets.""" + if child in self._children: + index = self._children.index(child) + del self._children[index] + + def _add_child(self, child): + """A private method for adding children componenets.""" + if child not in self._children: + self._children.append(child) + + def __repr__(self): + return "" % self.name \ No newline at end of file diff --git a/IPython/utils/tests/test_traitlets.py b/IPython/utils/tests/test_traitlets.py index f9de690..1ca8f1a 100644 --- a/IPython/utils/tests/test_traitlets.py +++ b/IPython/utils/tests/test_traitlets.py @@ -31,7 +31,7 @@ from unittest import TestCase from IPython.utils.traitlets import ( HasTraitlets, MetaHasTraitlets, TraitletType, Any, Int, Long, Float, Complex, Str, Unicode, Bool, TraitletError, - Undefined, Type, Instance + Undefined, Type, Instance, This ) @@ -440,6 +440,44 @@ class TestInstance(TestCase): b = B() self.assertRaises(TraitletError, getattr, b, 'inst') + +class TestThis(TestCase): + + def test_this_class(self): + class Foo(HasTraitlets): + this = This + + f = Foo() + self.assertEquals(f.this, None) + g = Foo() + f.this = g + self.assertEquals(f.this, g) + self.assertRaises(TraitletError, setattr, f, 'this', 10) + + def test_this_inst(self): + class Foo(HasTraitlets): + this = This() + + f = Foo() + f.this = Foo() + self.assert_(isinstance(f.this, Foo)) + + def test_allow_none(self): + class Foo(HasTraitlets): + this = This(allow_none=False) + + f = Foo() + g = Foo() + f.this = g + self.assertEquals(f.this, g) + + f = Foo() + self.assertRaises(TraitletError, getattr, f, 'this') + + f = Foo() + self.assertRaises(TraitletError, setattr, f, 'this', None) + + class TraitletTestBase(TestCase): """A best testing class for basic traitlet types.""" diff --git a/IPython/utils/traitlets.py b/IPython/utils/traitlets.py index 07c2929..0dcccab 100644 --- a/IPython/utils/traitlets.py +++ b/IPython/utils/traitlets.py @@ -167,19 +167,24 @@ class TraitletType(object): dv = self.default_value return dv - def __get__(self, obj, cls=None): + def __get__(self, obj, cls=None, skipset=False): """Get the value of the traitlet by self.name for the instance. - + The creation of default values is deferred until this is called the first time. This is done so instances of the parent HasTraitlets will have their own default value instances. + + A default value is not validated until it is requested. Thus, if + you use an invalid default value, but never request it, you are fine. """ if obj is None: return self else: if not obj._traitlet_values.has_key(self.name): dv = self.get_default_value() - self.__set__(obj, dv, first=True) + # Call __set__ with first=True so we don't get a recursion + if not skipset: + self.__set__(obj, dv, first=True) return dv else: return obj._traitlet_values[self.name] @@ -187,7 +192,8 @@ class TraitletType(object): def __set__(self, obj, value, first=False): new_value = self._validate(obj, value) if not first: - old_value = self.__get__(obj) + # Call __get__ with skipset=True so we don't get a recursion + old_value = self.__get__(obj, skipset=True) if old_value != new_value: obj._traitlet_values[self.name] = new_value obj._notify_traitlet(self.name, old_value, new_value) @@ -659,6 +665,40 @@ class Instance(BaseClassResolver): return dv +class This(TraitletType): + """A traitlet for instances of the class containing this trait.""" + + info_text = 'an instance of the same type as the receiver' + + def __init__(self, default_value=None, allow_none=True, **metadata): + if default_value is not None: + raise TraitletError("The default value of 'This' can only be None.") + super(This, self).__init__(default_value, **metadata) + self._allow_none = allow_none + if allow_none: + self.info_text = self.info_text + ' or None' + + def validate(self, obj, value): + if value is None: + if self._allow_none: + return value + self.validate_failed(obj, value) + + if isinstance(value, obj.__class__): + return value + else: + self.validate_failed(obj, value) + + def validate_failed (self, obj, value): + kind = type(value) + if kind is InstanceType: + msg = 'class %s' % value.__class__.__name__ + else: + msg = '%s (i.e. %s)' % ( str( kind )[1:-1], repr( value ) ) + + self.error(obj, msg) + + #----------------------------------------------------------------------------- # Basic TraitletTypes implementations/subclasses #-----------------------------------------------------------------------------