configurable.py
282 lines
| 10.5 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2731 | #!/usr/bin/env python | |
# encoding: utf-8 | |||
""" | |||
A base class for objects that are configurable. | |||
Authors: | |||
* Brian Granger | |||
* Fernando Perez | |||
MinRK
|
r4018 | * Min RK | |
Brian Granger
|
r2731 | """ | |
#----------------------------------------------------------------------------- | |||
MinRK
|
r4018 | # Copyright (C) 2008-2011 The IPython Development Team | |
Brian Granger
|
r2731 | # | |
# 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 | |||
MinRK
|
r4020 | from copy import deepcopy | |
Brian Granger
|
r2731 | ||
from loader import Config | |||
from IPython.utils.traitlets import HasTraits, Instance | |||
MinRK
|
r4020 | from IPython.utils.text import indent, wrap_paragraphs | |
Brian Granger
|
r2731 | ||
#----------------------------------------------------------------------------- | |||
# Helper classes for Configurables | |||
#----------------------------------------------------------------------------- | |||
class ConfigurableError(Exception): | |||
pass | |||
Brian Granger
|
r3792 | class MultipleInstanceError(ConfigurableError): | |
pass | |||
Brian Granger
|
r2731 | #----------------------------------------------------------------------------- | |
# Configurable implementation | |||
#----------------------------------------------------------------------------- | |||
class Configurable(HasTraits): | |||
config = Instance(Config,(),{}) | |||
created = None | |||
Brian Granger
|
r2740 | def __init__(self, **kwargs): | |
MinRK
|
r4020 | """Create a configurable given a config config. | |
Brian Granger
|
r2731 | ||
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. | |||
""" | |||
Brian Granger
|
r2740 | config = kwargs.pop('config', None) | |
Brian Granger
|
r2731 | 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 | |||
Brian Granger
|
r2740 | # This should go second so individual keyword arguments override | |
# the values in config. | |||
super(Configurable, self).__init__(**kwargs) | |||
Brian Granger
|
r2731 | 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] | |||
Thomas Kluyver
|
r3114 | for k, v in traits.iteritems(): | |
Brian Granger
|
r2731 | # 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)) | |||
Brian Granger
|
r3789 | @classmethod | |
def class_get_help(cls): | |||
Brian Granger
|
r3790 | """Get the help string for this class in ReST format.""" | |
Brian Granger
|
r3789 | cls_traits = cls.class_traits(config=True) | |
final_help = [] | |||
Brian Granger
|
r3790 | final_help.append(u'%s options' % cls.__name__) | |
final_help.append(len(final_help[0])*u'-') | |||
MinRK
|
r3944 | for k,v in cls.class_traits(config=True).iteritems(): | |
help = cls.class_get_trait_help(v) | |||
final_help.append(help) | |||
Brian Granger
|
r3789 | return '\n'.join(final_help) | |
MinRK
|
r3944 | ||
@classmethod | |||
def class_get_trait_help(cls, trait): | |||
MinRK
|
r4020 | """Get the help string for a single trait.""" | |
MinRK
|
r3944 | lines = [] | |
header = "%s.%s : %s" % (cls.__name__, trait.name, trait.__class__.__name__) | |||
MinRK
|
r4020 | lines.append(header) | |
MinRK
|
r3944 | try: | |
dvr = repr(trait.get_default_value()) | |||
except Exception: | |||
dvr = None # ignore defaults we can't construct | |||
if dvr is not None: | |||
MinRK
|
r4020 | 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,))) | |||
MinRK
|
r3944 | ||
help = trait.get_metadata('help') | |||
if help is not None: | |||
MinRK
|
r4020 | help = '\n'.join(wrap_paragraphs(help, 76)) | |
lines.append(indent(help, 4)) | |||
MinRK
|
r3944 | return '\n'.join(lines) | |
Brian Granger
|
r3789 | ||
@classmethod | |||
def class_print_help(cls): | |||
MinRK
|
r4020 | """Get the help string for a single trait and print it.""" | |
Brian Granger
|
r3789 | print cls.class_get_help() | |
Brian Granger
|
r3792 | ||
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 | |||
MinRK
|
r3961 | ||
@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 | |||
Brian Granger
|
r3792 | @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 | |||
MinRK
|
r3961 | # parent classes' _instance attribute. | |
for subclass in cls._walk_mro(): | |||
subclass._instance = inst | |||
Brian Granger
|
r3792 | if isinstance(cls._instance, cls): | |
return cls._instance | |||
else: | |||
raise MultipleInstanceError( | |||
'Multiple incompatible subclass instances of ' | |||
'%s are being created.' % cls.__name__ | |||
) | |||
Brian Granger
|
r3793 | ||
@classmethod | |||
def initialized(cls): | |||
"""Has an instance been created?""" | |||
return hasattr(cls, "_instance") and cls._instance is not None | |||
MinRK
|
r4016 | ||
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 | |||