configurable.py
366 lines
| 13.3 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2731 | # encoding: utf-8 | ||
MinRK
|
r17057 | """A base class for objects that are configurable.""" | ||
Brian Granger
|
r2731 | |||
MinRK
|
r17057 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Thomas Kluyver
|
r8795 | |||
Thomas Kluyver
|
r13348 | from __future__ import print_function | ||
Brian Granger
|
r2731 | |||
MinRK
|
r15854 | import logging | ||
MinRK
|
r4020 | from copy import deepcopy | ||
Brian Granger
|
r2731 | |||
MinRK
|
r12795 | from .loader import Config, LazyConfigValue | ||
Brian Granger
|
r2731 | from IPython.utils.traitlets import HasTraits, Instance | ||
MinRK
|
r4020 | from IPython.utils.text import indent, wrap_paragraphs | ||
Thomas Kluyver
|
r13361 | from IPython.utils.py3compat import iteritems | ||
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): | ||||
Matthias BUSSONNIER
|
r8885 | config = Instance(Config, (), {}) | ||
MinRK
|
r11062 | parent = Instance('IPython.config.configurable.Configurable') | ||
Brian Granger
|
r2731 | |||
Brian Granger
|
r2740 | def __init__(self, **kwargs): | ||
MinRK
|
r4020 | """Create a configurable given a config config. | ||
Brian Granger
|
r2731 | |||
Parameters | ||||
---------- | ||||
config : Config | ||||
Bernardo B. Marques
|
r4872 | If this is empty, default values are used. If config is a | ||
Brian Granger
|
r2731 | :class:`Config` instance, it will be used to configure the | ||
instance. | ||||
MinRK
|
r11069 | parent : Configurable instance, optional | ||
The parent Configurable instance of this object. | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2731 | Notes | ||
----- | ||||
Subclasses of Configurable must call the :meth:`__init__` method of | ||||
Bernardo B. Marques
|
r4872 | :class:`Configurable` *before* doing anything else and using | ||
Brian Granger
|
r2731 | :func:`super`:: | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2731 | class MyConfigurable(Configurable): | ||
def __init__(self, config=None): | ||||
Matthias BUSSONNIER
|
r8885 | super(MyConfigurable, self).__init__(config=config) | ||
Brian Granger
|
r2731 | # Then any other code you need to finish initialization. | ||
This ensures that instances will be configured properly. | ||||
""" | ||||
MinRK
|
r11062 | parent = kwargs.pop('parent', None) | ||
MinRK
|
r11070 | if parent is not None: | ||
MinRK
|
r11062 | # config is implied from parent | ||
MinRK
|
r11063 | if kwargs.get('config', None) is None: | ||
MinRK
|
r11062 | kwargs['config'] = parent.config | ||
self.parent = parent | ||||
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 | ||||
Bernardo B. Marques
|
r4872 | # This should go second so individual keyword arguments override | ||
Brian Granger
|
r2740 | # the values in config. | ||
super(Configurable, self).__init__(**kwargs) | ||||
Brian Granger
|
r2731 | |||
#------------------------------------------------------------------------- | ||||
# Static trait notifiations | ||||
#------------------------------------------------------------------------- | ||||
MinRK
|
r11062 | |||
@classmethod | ||||
def section_names(cls): | ||||
"""return section names as a list""" | ||||
return [c.__name__ for c in reversed(cls.__mro__) if | ||||
issubclass(c, Configurable) and issubclass(cls, c) | ||||
] | ||||
MinRK
|
r11069 | |||
def _find_my_config(self, cfg): | ||||
"""extract my config from a global Config object | ||||
will construct a Config object of only the config values that apply to me | ||||
based on my mro(), as well as those of my parent(s) if they exist. | ||||
If I am Bar and my parent is Foo, and their parent is Tim, | ||||
this will return merge following config sections, in this order:: | ||||
[Bar, Foo.bar, Tim.Foo.Bar] | ||||
With the last item being the highest priority. | ||||
""" | ||||
cfgs = [cfg] | ||||
if self.parent: | ||||
cfgs.append(self.parent._find_my_config(cfg)) | ||||
my_config = Config() | ||||
for c in cfgs: | ||||
for sname in self.section_names(): | ||||
# Don't do a blind getattr as that would cause the config to | ||||
# dynamically create the section with name Class.__name__. | ||||
if c._has_section(sname): | ||||
my_config.merge(c[sname]) | ||||
return my_config | ||||
MinRK
|
r12795 | |||
MinRK
|
r11062 | def _load_config(self, cfg, section_names=None, traits=None): | ||
"""load traits from a Config object""" | ||||
if traits is None: | ||||
traits = self.traits(config=True) | ||||
if section_names is None: | ||||
section_names = self.section_names() | ||||
MinRK
|
r11069 | my_config = self._find_my_config(cfg) | ||
Thomas Kluyver
|
r13361 | for name, config_value in iteritems(my_config): | ||
MinRK
|
r11069 | if name in traits: | ||
MinRK
|
r12795 | if isinstance(config_value, LazyConfigValue): | ||
# ConfigValue is a wrapper for using append / update on containers | ||||
# without having to copy the | ||||
initial = getattr(self, name) | ||||
config_value = config_value.get_value(initial) | ||||
MinRK
|
r11069 | # 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, name, deepcopy(config_value)) | ||||
Brian Granger
|
r2731 | |||
MinRK
|
r11062 | 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 = self.section_names() | ||||
self._load_config(new, traits=traits, section_names=section_names) | ||||
MinRK
|
r5223 | def update_config(self, config): | ||
"""Fire the traits events when the config is updated.""" | ||||
# Save a copy of the current config. | ||||
newconfig = deepcopy(self.config) | ||||
# Merge the new config into the current one. | ||||
MinRK
|
r10873 | newconfig.merge(config) | ||
MinRK
|
r5223 | # Save the combined config as self.config, which triggers the traits | ||
# events. | ||||
self.config = newconfig | ||||
Brian Granger
|
r3789 | @classmethod | ||
MinRK
|
r5224 | def class_get_help(cls, inst=None): | ||
"""Get the help string for this class in ReST format. | ||||
If `inst` is given, it's current trait values will be used in place of | ||||
class defaults. | ||||
""" | ||||
assert inst is None or isinstance(inst, cls) | ||||
Brian Granger
|
r3789 | final_help = [] | ||
Brian Granger
|
r3790 | final_help.append(u'%s options' % cls.__name__) | ||
final_help.append(len(final_help[0])*u'-') | ||||
Thomas Kluyver
|
r13361 | for k, v in sorted(cls.class_traits(config=True).items()): | ||
MinRK
|
r5224 | help = cls.class_get_trait_help(v, inst) | ||
MinRK
|
r3944 | final_help.append(help) | ||
Brian Granger
|
r3789 | return '\n'.join(final_help) | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3944 | @classmethod | ||
MinRK
|
r5224 | def class_get_trait_help(cls, trait, inst=None): | ||
"""Get the help string for a single trait. | ||||
If `inst` is given, it's current trait values will be used in place of | ||||
the class default. | ||||
""" | ||||
assert inst is None or isinstance(inst, cls) | ||||
MinRK
|
r3944 | lines = [] | ||
MinRK
|
r4189 | header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__) | ||
MinRK
|
r4020 | lines.append(header) | ||
MinRK
|
r5224 | if inst is not None: | ||
lines.append(indent('Current: %r' % getattr(inst, trait.name), 4)) | ||||
else: | ||||
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)) | ||||
MinRK
|
r4020 | if 'Enum' in trait.__class__.__name__: | ||
# include Enum choices | ||||
MinRK
|
r5224 | lines.append(indent('Choices: %r' % (trait.values,))) | ||
Bernardo B. Marques
|
r4872 | |||
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 | ||||
MinRK
|
r5224 | def class_print_help(cls, inst=None): | ||
MinRK
|
r4020 | """Get the help string for a single trait and print it.""" | ||
Thomas Kluyver
|
r13348 | print(cls.class_get_help(inst)) | ||
Brian Granger
|
r3789 | |||
MinRK
|
r4025 | @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)) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4025 | return '# ' + s.replace('\n', '\n# ') | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4025 | # section header | ||
breaker = '#' + '-'*78 | ||||
Matthias BUSSONNIER
|
r8885 | s = "# %s configuration" % cls.__name__ | ||
MinRK
|
r4025 | 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('') | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4075 | parents = [] | ||
for parent in cls.mro(): | ||||
# only include parents that are not base classes | ||||
# and are not the class itself | ||||
MinRK
|
r4849 | # and have some configurable traits to inherit | ||
if parent is not cls and issubclass(parent, Configurable) and \ | ||||
parent.class_traits(config=True): | ||||
MinRK
|
r4075 | parents.append(parent) | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4075 | if parents: | ||
pstr = ', '.join([ p.__name__ for p in parents ]) | ||||
lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr))) | ||||
lines.append('') | ||||
Bernardo B. Marques
|
r4872 | |||
Thomas Kluyver
|
r13361 | for name, trait in iteritems(cls.class_traits(config=True)): | ||
MinRK
|
r4025 | 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) | ||||
Bernardo B. Marques
|
r4872 | |||
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 | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3961 | @classmethod | ||
def _walk_mro(cls): | ||||
"""Walk the cls.mro() for parent classes that are also singletons | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3961 | For use in instance() | ||
""" | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3961 | for subclass in cls.mro(): | ||
if issubclass(cls, subclass) and \ | ||||
issubclass(subclass, SingletonConfigurable) and \ | ||||
subclass != SingletonConfigurable: | ||||
yield subclass | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r3961 | @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 | ||||
Bernardo B. Marques
|
r4872 | |||
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 | ||||
Bernardo B. Marques
|
r4872 | |||
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. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4016 | Subclasses have a log trait, and the default behavior | ||
MinRK
|
r17057 | is to get the logger from the currently running Application. | ||
MinRK
|
r4016 | """ | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4016 | log = Instance('logging.Logger') | ||
def _log_default(self): | ||||
MinRK
|
r17057 | from IPython.utils import log | ||
return log.get_logger() | ||||
Bernardo B. Marques
|
r4872 | |||