Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config5 #400

Merged
merged 11 commits into from May 17, 2011
134 changes: 134 additions & 0 deletions IPython/config/application.py
@@ -0,0 +1,134 @@
# encoding: utf-8
"""
A base class for a configurable application.

Authors:

* Brian Granger
"""

#-----------------------------------------------------------------------------
# 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
#-----------------------------------------------------------------------------

from copy import deepcopy
import logging
import sys

from IPython.config.configurable import SingletonConfigurable
from IPython.utils.traitlets import (
Unicode, List, Int, Enum
)
from IPython.config.loader import (
KeyValueConfigLoader, PyFileConfigLoader
)

#-----------------------------------------------------------------------------
# Application class
#-----------------------------------------------------------------------------


class Application(SingletonConfigurable):
"""A singleton application with full configuration support."""

# The name of the application, will usually match the name of the command
# line application
app_name = Unicode(u'application')

# The description of the application that is printed at the beginning
# of the help.
description = Unicode(u'This is an application.')

# A sequence of Configurable subclasses whose config=True attributes will
# be exposed at the command line (shortnames and help).
classes = List([])

# The version string of this application.
version = Unicode(u'0.0')

# The log level for the application
log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN,
config=True, shortname="log_level",
help="Set the log level (0,10,20,30,40,50).")

def __init__(self, **kwargs):
SingletonConfigurable.__init__(self, **kwargs)
# Add my class to self.classes so my attributes appear in command line
# options.
self.classes.insert(0, self.__class__)
self.init_logging()

def init_logging(self):
"""Start logging for this application.

The default is to log to stdout using a StreaHandler. The log level
starts at loggin.WARN, but this can be adjusted by setting the
``log_level`` attribute.
"""
self.log = logging.getLogger(self.__class__.__name__)
self.log.setLevel(self.log_level)
self._log_handler = logging.StreamHandler()
self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
self._log_handler.setFormatter(self._log_formatter)
self.log.addHandler(self._log_handler)

def _log_level_changed(self, name, old, new):
"""Adjust the log level when log_level is set."""
self.log.setLevel(new)

def print_help(self):
"""Print the help for each Configurable class in self.classes."""
for cls in self.classes:
cls.class_print_help()
print

def print_description(self):
"""Print the application description."""
print self.description
print

def print_version(self):
"""Print the version string."""
print self.version

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.
newconfig._merge(config)
# Save the combined config as self.config, which triggers the traits
# events.
self.config = config

def parse_command_line(self, argv=None):
"""Parse the command line arguments."""
argv = sys.argv[1:] if argv is None else argv

if '-h' in argv or '--h' in argv:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'--help' should probably trigger here as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, it should probably be '-h', '--help'. I don't think anyone expects '--' with abbreviated names.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

self.print_description()
self.print_help()
sys.exit(1)

if '--version' in argv:
self.print_version()
sys.exit(1)

loader = KeyValueConfigLoader(argv=argv, classes=self.classes)
config = loader.load_config()
self.update_config(config)

def load_config_file(self, filename, path=None):
"""Load a .py based config file by filename and path."""
# TODO: this raises IOError if filename does not exist.
loader = PyFileConfigLoader(filename, path=path)
config = loader.load_config()
self.update_config(config)

105 changes: 103 additions & 2 deletions IPython/config/configurable.py
Expand Up @@ -22,11 +22,10 @@

from copy import deepcopy
import datetime
from weakref import WeakValueDictionary

from IPython.utils.importstring import import_item
from loader import Config
from IPython.utils.traitlets import HasTraits, Instance
from IPython.utils.text import indent


#-----------------------------------------------------------------------------
Expand All @@ -38,6 +37,9 @@ class ConfigurableError(Exception):
pass


class MultipleInstanceError(ConfigurableError):
pass

#-----------------------------------------------------------------------------
# Configurable implementation
#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -137,3 +139,102 @@ def _config_changed(self, name, old, new):
# shared by all instances, effectively making it a class attribute.
setattr(self, k, deepcopy(config_value))

@classmethod
def class_get_shortnames(cls):
"""Return the shortname to fullname dict for config=True traits."""
cls_traits = cls.class_traits(config=True)
shortnames = {}
for k, v in cls_traits.items():
shortname = v.get_metadata('shortname')
if shortname is not None:
longname = cls.__name__ + '.' + k
shortnames[shortname] = longname
return shortnames

@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_traits.items():
help = v.get_metadata('help')
shortname = v.get_metadata('shortname')
header = "%s.%s : %s" % (cls.__name__, k, v.__class__.__name__)
if shortname is not None:
header += " (shortname=" + shortname + ")"
final_help.append(header)
if help is not None:
final_help.append(indent(help))
return '\n'.join(final_help)

@classmethod
def class_print_help(cls):
print cls.class_get_help()


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 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
# the subclasses instance attribute.
for subclass in cls.mro():
if issubclass(cls, subclass) and \
issubclass(subclass, SingletonConfigurable) and \
subclass != SingletonConfigurable:
subclass._instance = inst
else:
break
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