##// END OF EJS Templates

File last commit:

r4862:afbc4511
r4870:fcafc407
Show More
configurable.py
328 lines | 12.2 KiB | text/x-python | PythonLexer
# encoding: utf-8
"""
A base class for objects that are configurable.
Authors:
* Brian Granger
* Fernando Perez
* Min RK
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2008-2011 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
#-----------------------------------------------------------------------------
import datetime
from copy import deepcopy
from loader import Config
from IPython.utils.traitlets import HasTraits, Instance
from IPython.utils.text import indent, wrap_paragraphs
#-----------------------------------------------------------------------------
# Helper classes for Configurables
#-----------------------------------------------------------------------------
class ConfigurableError(Exception):
pass
class MultipleInstanceError(ConfigurableError):
pass
#-----------------------------------------------------------------------------
# Configurable implementation
#-----------------------------------------------------------------------------
class Configurable(HasTraits):
config = Instance(Config,(),{})
created = None
def __init__(self, **kwargs):
"""Create a configurable given a config config.
Parameters
----------
config : Config
If this is empty, default values are used. If config is a
:class:`Config` instance, it will be used to configure the
instance.
Notes
-----
Subclasses of Configurable must call the :meth:`__init__` method of
:class:`Configurable` *before* doing anything else and using
:func:`super`::
class MyConfigurable(Configurable):
def __init__(self, config=None):
super(MyConfigurable, self).__init__(config)
# Then any other code you need to finish initialization.
This ensures that instances will be configured properly.
"""
config = kwargs.pop('config', None)
if config is not None:
# 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)
self.config = config
# This should go second so individual keyword arguments override
# the values in config.
super(Configurable, self).__init__(**kwargs)
self.created = datetime.datetime.now()
#-------------------------------------------------------------------------
# Static trait notifiations
#-------------------------------------------------------------------------
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 Configurable subclasses. This starts with Configurable
# 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, Configurable) 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.iteritems():
# 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 ConfigurableError('Configurable 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))
@classmethod
def class_get_help(cls):
"""Get the help string for this class in ReST format."""
cls_traits = cls.class_traits(config=True)
final_help = []
final_help.append(u'%s options' % cls.__name__)
final_help.append(len(final_help[0])*u'-')
for k,v in cls.class_traits(config=True).iteritems():
help = cls.class_get_trait_help(v)
final_help.append(help)
return '\n'.join(final_help)
@classmethod
def class_get_trait_help(cls, trait):
"""Get the help string for a single trait."""
lines = []
header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
lines.append(header)
try:
dvr = repr(trait.get_default_value())
except Exception:
dvr = None # ignore defaults we can't construct
if dvr is not None:
if len(dvr) > 64:
dvr = dvr[:61]+'...'
lines.append(indent('Default: %s'%dvr, 4))
if 'Enum' in trait.__class__.__name__:
# include Enum choices
lines.append(indent('Choices: %r'%(trait.values,)))
help = trait.get_metadata('help')
if help is not None:
help = '\n'.join(wrap_paragraphs(help, 76))
lines.append(indent(help, 4))
return '\n'.join(lines)
@classmethod
def class_print_help(cls):
"""Get the help string for a single trait and print it."""
print cls.class_get_help()
@classmethod
def class_config_section(cls):
"""Get the config class config section"""
def c(s):
"""return a commented, wrapped block."""
s = '\n\n'.join(wrap_paragraphs(s, 78))
return '# ' + s.replace('\n', '\n# ')
# section header
breaker = '#' + '-'*78
s = "# %s configuration"%cls.__name__
lines = [breaker, s, breaker, '']
# get the description trait
desc = cls.class_traits().get('description')
if desc:
desc = desc.default_value
else:
# no description trait, use __doc__
desc = getattr(cls, '__doc__', '')
if desc:
lines.append(c(desc))
lines.append('')
parents = []
for parent in cls.mro():
# only include parents that are not base classes
# and are not the class itself
# and have some configurable traits to inherit
if parent is not cls and issubclass(parent, Configurable) and \
parent.class_traits(config=True):
parents.append(parent)
if parents:
pstr = ', '.join([ p.__name__ for p in parents ])
lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
lines.append('')
for name,trait in cls.class_traits(config=True).iteritems():
help = trait.get_metadata('help') or ''
lines.append(c(help))
lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
lines.append('')
return '\n'.join(lines)
class SingletonConfigurable(Configurable):
"""A configurable that only allows one instance.
This class is for classes that should only have one instance of itself
or *any* subclass. To create and retrieve such a class use the
:meth:`SingletonConfigurable.instance` method.
"""
_instance = None
@classmethod
def _walk_mro(cls):
"""Walk the cls.mro() for parent classes that are also singletons
For use in instance()
"""
for subclass in cls.mro():
if issubclass(cls, subclass) and \
issubclass(subclass, SingletonConfigurable) and \
subclass != SingletonConfigurable:
yield subclass
@classmethod
def clear_instance(cls):
"""unset _instance for this class and singleton parents.
"""
if not cls.initialized():
return
for subclass in cls._walk_mro():
if isinstance(subclass._instance, cls):
# only clear instances that are instances
# of the calling class
subclass._instance = None
@classmethod
def instance(cls, *args, **kwargs):
"""Returns a global instance of this class.
This method create a new instance if none have previously been created
and returns a previously created instance is one already exists.
The arguments and keyword arguments passed to this method are passed
on to the :meth:`__init__` method of the class upon instantiation.
Examples
--------
Create a singleton class using instance, and retrieve it::
>>> from IPython.config.configurable import SingletonConfigurable
>>> class Foo(SingletonConfigurable): pass
>>> foo = Foo.instance()
>>> foo == Foo.instance()
True
Create a subclass that is retrived using the base class instance::
>>> class Bar(SingletonConfigurable): pass
>>> class Bam(Bar): pass
>>> bam = Bam.instance()
>>> bam == Bar.instance()
True
"""
# Create and save the instance
if cls._instance is None:
inst = cls(*args, **kwargs)
# Now make sure that the instance will also be returned by
# parent classes' _instance attribute.
for subclass in cls._walk_mro():
subclass._instance = inst
if isinstance(cls._instance, cls):
return cls._instance
else:
raise MultipleInstanceError(
'Multiple incompatible subclass instances of '
'%s are being created.' % cls.__name__
)
@classmethod
def initialized(cls):
"""Has an instance been created?"""
return hasattr(cls, "_instance") and cls._instance is not None
class LoggingConfigurable(Configurable):
"""A parent class for Configurables that log.
Subclasses have a log trait, and the default behavior
is to get the logger from the currently running Application
via Application.instance().log.
"""
log = Instance('logging.Logger')
def _log_default(self):
from IPython.config.application import Application
return Application.instance().log