##// END OF EJS Templates
Finish cleanup of setup.py and tests after dead code removal....
Finish cleanup of setup.py and tests after dead code removal. This completes the cleanup from Brian's previous commits, so that trunk again installs/tests correctly.

File last commit:

r2537:535d8779 merge
r2662:ce832fe7
Show More
component.py
346 lines | 13.3 KiB | text/x-python | PythonLexer
#!/usr/bin/env python
# encoding: utf-8
"""
A lightweight component system for IPython.
Authors:
* Brian Granger
* Fernando Perez
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2008-2009 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
from copy import deepcopy
import datetime
from weakref import WeakValueDictionary
from IPython.utils.importstring import import_item
from IPython.config.loader import Config
from IPython.utils.traitlets import (
HasTraits, MetaHasTraits, Instance, This
)
#-----------------------------------------------------------------------------
# Helper classes for Components
#-----------------------------------------------------------------------------
class ComponentError(Exception):
pass
class MetaComponentTracker(type):
"""A metaclass that tracks instances of Components and its subclasses."""
def __init__(cls, name, bases, d):
super(MetaComponentTracker, cls).__init__(name, bases, d)
cls.__instance_refs = WeakValueDictionary()
cls.__numcreated = 0
def __call__(cls, *args, **kw):
"""Called when a class is called (instantiated)!!!
When a Component or subclass is instantiated, this is called and
the instance is saved in a WeakValueDictionary for tracking.
"""
instance = cls.__new__(cls, *args, **kw)
# Register the instance before __init__ is called so get_instances
# works inside __init__ methods!
indices = cls.register_instance(instance)
# This is in a try/except because of the __init__ method fails, the
# instance is discarded and shouldn't be tracked.
try:
if isinstance(instance, cls):
cls.__init__(instance, *args, **kw)
except:
# Unregister the instance because __init__ failed!
cls.unregister_instances(indices)
raise
else:
return instance
def register_instance(cls, instance):
"""Register instance with cls and its subclasses."""
# indices is a list of the keys used to register the instance
# with. This list is needed if the instance needs to be unregistered.
indices = []
for c in cls.__mro__:
if issubclass(cls, c) and issubclass(c, Component):
c.__numcreated += 1
indices.append(c.__numcreated)
c.__instance_refs[c.__numcreated] = instance
else:
break
return indices
def unregister_instances(cls, indices):
"""Unregister instance with cls and its subclasses."""
for c, index in zip(cls.__mro__, indices):
try:
del c.__instance_refs[index]
except KeyError:
pass
def clear_instances(cls):
"""Clear all instances tracked by cls."""
cls.__instance_refs.clear()
cls.__numcreated = 0
def get_instances(cls, name=None, root=None, klass=None):
"""Get all instances of cls and its subclasses.
Parameters
----------
name : str
Limit to components with this name.
root : Component or subclass
Limit to components having this root.
klass : class or str
Limits to instances of the class or its subclasses. If a str
is given ut must be in the form 'foo.bar.MyClass'. The str
form of this argument is useful for forward declarations.
"""
if klass is not None:
if isinstance(klass, basestring):
klass = import_item(klass)
# Limit search to instances of klass for performance
if issubclass(klass, Component):
return klass.get_instances(name=name, root=root)
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, root=None,
klass=None):
"""Get all instances of cls, i such that call(i)==True.
This also takes the ``name`` and ``root`` and ``classname``
arguments of :meth:`get_instance`
"""
return [i for i in cls.get_instances(name, root, klass) if call(i)]
def masquerade_as(instance, cls):
"""Let instance masquerade as an instance of cls.
Sometimes, such as in testing code, it is useful to let a class
masquerade as another. Python, being duck typed, allows this by
default. But, instances of components are tracked by their class type.
After calling this, ``cls.get_instances()`` will return ``instance``. This
does not, however, cause ``isinstance(instance, cls)`` to return ``True``.
Parameters
----------
instance : an instance of a Component or Component subclass
The instance that will pretend to be a cls.
cls : subclass of Component
The Component subclass that instance will pretend to be.
"""
cls.register_instance(instance)
class __ComponentNameGenerator(object):
"""A Singleton to generate unique component names."""
def __init__(self, prefix):
self.prefix = prefix
self.i = 0
def __call__(self):
count = self.i
self.i += 1
return "%s%s" % (self.prefix, count)
ComponentNameGenerator = __ComponentNameGenerator('ipython.component')
class MetaComponent(MetaHasTraits, MetaComponentTracker):
pass
#-----------------------------------------------------------------------------
# Component implementation
#-----------------------------------------------------------------------------
class Component(HasTraits):
__metaclass__ = MetaComponent
# Traits are fun!
config = Instance(Config,(),{})
parent = This()
root = This()
created = None
def __init__(self, parent, name=None, config=None):
"""Create a component given a parent and possibly and name and config.
Parameters
----------
parent : Component subclass
The parent in the component graph. The parent is used
to get the root of the component graph.
name : str
The unique name of the component. If empty, then a unique
one will be autogenerated.
config : Config
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
parent.config. If a caller wants to modify parent.config
(not override), the caller should make a copy and change
attributes and then pass the copy to this argument.
Notes
-----
Subclasses of Component must call the :meth:`__init__` method of
:class:`Component` *before* doing anything else and using
:func:`super`::
class MyComponent(Component):
def __init__(self, parent, name=None, config=None):
super(MyComponent, self).__init__(parent, name, config)
# Then any other code you need to finish initialization.
This ensures that the :attr:`parent`, :attr:`name` and :attr:`config`
attributes are handled properly.
"""
super(Component, self).__init__()
self._children = []
if name is None:
self.name = ComponentNameGenerator()
else:
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
# We used to deepcopy, but for now we are trying to just save
# by reference. This *could* have side effects as all components
# will share config. In fact, I did find such a side effect in
# _config_changed below. If a config attribute value was a mutable type
# all instances of a component were getting the same copy, effectively
# making that a class attribute.
# self.config = deepcopy(config)
else:
if self.parent is not None:
self.config = self.parent.config
# We used to deepcopy, but for now we are trying to just save
# by reference. This *could* have side effects as all components
# will share config. In fact, I did find such a side effect in
# _config_changed below. If a config attribute value was a mutable type
# all instances of a component were getting the same copy, effectively
# making that a class attribute.
# self.config = deepcopy(self.parent.config)
self.created = datetime.datetime.now()
#-------------------------------------------------------------------------
# Static trait notifiations
#-------------------------------------------------------------------------
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)
if new is None:
self.root = self
else:
self.root = new.root
def _root_changed(self, name, old, new):
if self.parent is None:
if not (new is self):
raise ComponentError("Root not self, but parent is None.")
else:
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):
"""Update all the class traits having ``config=True`` as metadata.
For any class trait with a ``config`` metadata attribute that is
``True``, we update the trait with the value of the corresponding
config entry.
"""
# Get all traits with a config metadata entry that is True
traits = self.traits(config=True)
# We auto-load config section for this class as well as any parent
# classes that are Component subclasses. This starts with Component
# and works down the mro loading the config for each section.
section_names = [cls.__name__ for cls in \
reversed(self.__class__.__mro__) if
issubclass(cls, Component) and issubclass(self.__class__, cls)]
for sname in section_names:
# Don't do a blind getattr as that would cause the config to
# dynamically create the section with name self.__class__.__name__.
if new._has_section(sname):
my_config = new[sname]
for k, v in traits.items():
# Don't allow traitlets with config=True to start with
# uppercase. Otherwise, they are confused with Config
# subsections. But, developers shouldn't have uppercase
# attributes anyways! (PEP 6)
if k[0].upper()==k[0] and not k.startswith('_'):
raise ComponentError('Component traitlets with '
'config=True must start with a lowercase so they are '
'not confused with Config subsections: %s.%s' % \
(self.__class__.__name__, k))
try:
# Here we grab the value from the config
# If k has the naming convention of a config
# section, it will be auto created.
config_value = my_config[k]
except KeyError:
pass
else:
# print "Setting %s.%s from %s.%s=%r" % \
# (self.__class__.__name__,k,sname,k,config_value)
# We have to do a deepcopy here if we don't deepcopy the entire
# config object. If we don't, a mutable config_value will be
# shared by all instances, effectively making it a class attribute.
setattr(self, k, deepcopy(config_value))
@property
def children(self):
"""A list of all my child components."""
return self._children
def _remove_child(self, child):
"""A private method for removing children components."""
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 components."""
if child not in self._children:
self._children.append(child)
def __repr__(self):
return "<%s('%s')>" % (self.__class__.__name__, self.name)