|
|
#!/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, TraitError, 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)
|
|
|
|