#!/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.ipstruct import Struct from IPython.utils.traitlets import ( HasTraitlets, TraitletError, MetaHasTraitlets, 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 *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 = super(MetaComponentTracker, cls).__call__(*args, **kw) for c in cls.__mro__: if issubclass(cls, c) and issubclass(c, Component): c.__numcreated += 1 c.__instance_refs[c.__numcreated] = instance return instance def get_instances(cls, name=None, root=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. """ instances = cls.__instance_refs.values() if name is not None: instances = [i for i in instances if i.name == name] 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): """Get all instances of cls, i such that call(i)==True. This also takes the ``name`` and ``root`` arguments of :meth:`get_instance` """ return [i for i in cls.get_instances(name, root) if call(i)] 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(MetaHasTraitlets, MetaComponentTracker): pass #----------------------------------------------------------------------------- # Component implementation #----------------------------------------------------------------------------- class Component(HasTraitlets): __metaclass__ = MetaComponent # Traitlets are fun! config = Instance(Struct,(),{}) 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 : 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 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 = deepcopy(config) else: if self.parent is not None: self.config = deepcopy(self.parent.config) self.created = datetime.datetime.now() #------------------------------------------------------------------------- # Static traitlet 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 a config_key with the config. For any class traitlet with a ``config_key`` metadata attribute, we update the traitlet with the value of the corresponding config entry. In the future, we might want to do a pop here so stale config info is not passed onto children. """ # Get all traitlets with a config_key metadata entry traitlets = self.traitlets('config_key') 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.""" 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 "" % self.name