application.py
361 lines
| 12.5 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r3790 | # 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 | ||||
Brian Granger
|
r3794 | import logging | ||
MinRK
|
r3952 | import re | ||
Brian Granger
|
r3790 | import sys | ||
Brian Granger
|
r3794 | from IPython.config.configurable import SingletonConfigurable | ||
Brian Granger
|
r3790 | from IPython.config.loader import ( | ||
MinRK
|
r3951 | KeyValueConfigLoader, PyFileConfigLoader, Config, ArgumentError | ||
Brian Granger
|
r3790 | ) | ||
MinRK
|
r3852 | from IPython.utils.traitlets import ( | ||
MinRK
|
r3949 | Unicode, List, Int, Enum, Dict, Instance | ||
MinRK
|
r3852 | ) | ||
MinRK
|
r3949 | from IPython.utils.importstring import import_item | ||
MinRK
|
r3852 | from IPython.utils.text import indent | ||
Brian Granger
|
r3790 | #----------------------------------------------------------------------------- | ||
MinRK
|
r3944 | # Descriptions for the various sections | ||
MinRK
|
r3855 | #----------------------------------------------------------------------------- | ||
MinRK
|
r3861 | flag_description = """ | ||
MinRK
|
r3855 | Flags are command-line arguments passed as '--<flag>'. | ||
These take no parameters, unlike regular key-value arguments. | ||||
They are typically used for setting boolean flags, or enabling | ||||
modes that involve setting multiple options together. | ||||
""".strip() # trim newlines of front and back | ||||
MinRK
|
r3861 | alias_description = """ | ||
MinRK
|
r3855 | These are commonly set parameters, given abbreviated aliases for convenience. | ||
They are set in the same `name=value` way as class parameters, where | ||||
<name> is replaced by the real parameter for which it is an alias. | ||||
""".strip() # trim newlines of front and back | ||||
keyvalue_description = """ | ||||
Parameters are set from command-line arguments of the form: | ||||
`Class.trait=value`. Parameters will *never* be prefixed with '-'. | ||||
This line is evaluated in Python, so simple expressions are allowed, e.g. | ||||
`C.a='range(3)'` For setting C.a=[0,1,2] | ||||
""".strip() # trim newlines of front and back | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r3790 | # Application class | ||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r3940 | class ApplicationError(Exception): | ||
pass | ||||
Brian Granger
|
r3794 | class Application(SingletonConfigurable): | ||
Brian Granger
|
r3796 | """A singleton application with full configuration support.""" | ||
Brian Granger
|
r3790 | |||
# The name of the application, will usually match the name of the command | ||||
# line application | ||||
Brian Granger
|
r3942 | name = Unicode(u'application') | ||
Brian Granger
|
r3790 | |||
# The description of the application that is printed at the beginning | ||||
# of the help. | ||||
description = Unicode(u'This is an application.') | ||||
MinRK
|
r3855 | # default section descriptions | ||
MinRK
|
r3861 | flag_description = Unicode(flag_description) | ||
alias_description = Unicode(alias_description) | ||||
MinRK
|
r3855 | keyvalue_description = Unicode(keyvalue_description) | ||
Brian Granger
|
r3790 | |||
# A sequence of Configurable subclasses whose config=True attributes will | ||||
MinRK
|
r3861 | # be exposed at the command line. | ||
Brian Granger
|
r3790 | classes = List([]) | ||
# The version string of this application. | ||||
version = Unicode(u'0.0') | ||||
Brian Granger
|
r3795 | # The log level for the application | ||
log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN, | ||||
MinRK
|
r3852 | config=True, | ||
MinRK
|
r3944 | help="Set the log level.") | ||
MinRK
|
r3852 | |||
MinRK
|
r3861 | # the alias map for configurables | ||
aliases = Dict(dict(log_level='Application.log_level')) | ||||
MinRK
|
r3852 | |||
MinRK
|
r3861 | # flags for loading Configurables or store_const style flags | ||
# flags are loaded from this dict by '--key' flags | ||||
# this must be a dict of two-tuples, the first element being the Config/dict | ||||
# and the second being the help string for the flag | ||||
flags = Dict() | ||||
MinRK
|
r3852 | |||
MinRK
|
r3949 | # subcommands for launching other applications | ||
# if this is not empty, this will be a parent Application | ||||
# this must be a dict of two-tuples, the first element being the application class/import string | ||||
# and the second being the help string for the subcommand | ||||
subcommands = Dict() | ||||
# parse_command_line will initialize a subapp, if requested | ||||
subapp = Instance('IPython.config.application.Application', allow_none=True) | ||||
MinRK
|
r3958 | # extra command-line arguments that don't set config values | ||
extra_args = List(Unicode) | ||||
Brian Granger
|
r3794 | |||
Brian Granger
|
r3790 | def __init__(self, **kwargs): | ||
Brian Granger
|
r3794 | SingletonConfigurable.__init__(self, **kwargs) | ||
Brian Granger
|
r3790 | # Add my class to self.classes so my attributes appear in command line | ||
# options. | ||||
self.classes.insert(0, self.__class__) | ||||
MinRK
|
r3852 | |||
MinRK
|
r3861 | # ensure self.flags dict is valid | ||
for key,value in self.flags.iteritems(): | ||||
assert len(value) == 2, "Bad flag: %r:%s"%(key,value) | ||||
assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value) | ||||
assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value) | ||||
Brian Granger
|
r3794 | self.init_logging() | ||
Brian Granger
|
r3940 | |||
def _config_changed(self, name, old, new): | ||||
SingletonConfigurable._config_changed(self, name, old, new) | ||||
self.log.debug('Config changed:') | ||||
self.log.debug(repr(new)) | ||||
Brian Granger
|
r3794 | 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) | ||||
MinRK
|
r3949 | |||
def initialize(self, argv=None): | ||||
"""Do the basic steps to configure me. | ||||
Override in subclasses. | ||||
""" | ||||
self.parse_command_line(argv) | ||||
def start(self): | ||||
"""Start the app mainloop. | ||||
Override in subclasses. | ||||
""" | ||||
if self.subapp is not None: | ||||
return self.subapp.start() | ||||
Brian Granger
|
r3794 | def _log_level_changed(self, name, old, new): | ||
"""Adjust the log level when log_level is set.""" | ||||
self.log.setLevel(new) | ||||
MinRK
|
r3852 | |||
MinRK
|
r3861 | def print_alias_help(self): | ||
"""print the alias part of the help""" | ||||
if not self.aliases: | ||||
MinRK
|
r3852 | return | ||
MinRK
|
r3944 | |||
lines = ['Aliases'] | ||||
MinRK
|
r3949 | lines.append('-'*len(lines[0])) | ||
MinRK
|
r3944 | lines.append(self.alias_description) | ||
lines.append('') | ||||
MinRK
|
r3855 | |||
MinRK
|
r3852 | classdict = {} | ||
MinRK
|
r3951 | for cls in self.classes: | ||
MinRK
|
r3952 | # include all parents (up to, but excluding Configurable) in available names | ||
for c in cls.mro()[:-3]: | ||||
MinRK
|
r3951 | classdict[c.__name__] = c | ||
MinRK
|
r3855 | |||
MinRK
|
r3861 | for alias, longname in self.aliases.iteritems(): | ||
MinRK
|
r3852 | classname, traitname = longname.split('.',1) | ||
cls = classdict[classname] | ||||
trait = cls.class_traits(config=True)[traitname] | ||||
MinRK
|
r3944 | help = cls.class_get_trait_help(trait) | ||
help = help.replace(longname, "%s (%s)"%(alias, longname), 1) | ||||
lines.append(help) | ||||
lines.append('') | ||||
print '\n'.join(lines) | ||||
MinRK
|
r3852 | |||
MinRK
|
r3861 | def print_flag_help(self): | ||
"""print the flag part of the help""" | ||||
if not self.flags: | ||||
MinRK
|
r3852 | return | ||
MinRK
|
r3944 | lines = ['Flags'] | ||
MinRK
|
r3949 | lines.append('-'*len(lines[0])) | ||
MinRK
|
r3944 | lines.append(self.flag_description) | ||
lines.append('') | ||||
MinRK
|
r3852 | |||
MinRK
|
r3861 | for m, (cfg,help) in self.flags.iteritems(): | ||
MinRK
|
r3944 | lines.append('--'+m) | ||
MinRK
|
r3959 | lines.append(indent(help.strip(), flatten=True)) | ||
MinRK
|
r3944 | lines.append('') | ||
print '\n'.join(lines) | ||||
MinRK
|
r3852 | |||
MinRK
|
r3949 | def print_subcommands(self): | ||
"""print the subcommand part of the help""" | ||||
if not self.subcommands: | ||||
return | ||||
lines = ["Subcommands"] | ||||
lines.append('-'*len(lines[0])) | ||||
MinRK
|
r3951 | for subc, (cls,help) in self.subcommands.iteritems(): | ||
MinRK
|
r3949 | lines.append("%s : %s"%(subc, cls)) | ||
if help: | ||||
MinRK
|
r3959 | lines.append(indent(help.strip(), flatten=True)) | ||
MinRK
|
r3949 | lines.append('') | ||
print '\n'.join(lines) | ||||
MinRK
|
r3946 | def print_help(self, classes=False): | ||
"""Print the help for each Configurable class in self.classes. | ||||
If classes=False (the default), only flags and aliases are printed | ||||
""" | ||||
MinRK
|
r3952 | self.print_subcommands() | ||
MinRK
|
r3861 | self.print_flag_help() | ||
self.print_alias_help() | ||||
MinRK
|
r3944 | |||
MinRK
|
r3946 | if classes: | ||
if self.classes: | ||||
print "Class parameters" | ||||
print "----------------" | ||||
print self.keyvalue_description | ||||
MinRK
|
r3855 | |||
MinRK
|
r3946 | for cls in self.classes: | ||
cls.class_print_help() | ||||
else: | ||||
print "To see all available configurables, use `--help-all`" | ||||
Brian Granger
|
r3790 | |||
def print_description(self): | ||||
"""Print the application description.""" | ||||
print self.description | ||||
def print_version(self): | ||||
"""Print the version string.""" | ||||
print self.version | ||||
def update_config(self, config): | ||||
Brian Granger
|
r3796 | """Fire the traits events when the config is updated.""" | ||
Brian Granger
|
r3790 | # 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. | ||||
Brian Granger
|
r3941 | self.config = newconfig | ||
MinRK
|
r3949 | |||
def initialize_subcommand(self, subc, argv=None): | ||||
"""Initialize a subcommand with argv""" | ||||
MinRK
|
r3958 | subapp,help = self.subcommands.get(subc) | ||
MinRK
|
r3951 | |||
MinRK
|
r3949 | if isinstance(subapp, basestring): | ||
subapp = import_item(subapp) | ||||
MinRK
|
r3962 | # clear existing instances | ||
self.__class__.clear_instance() | ||||
MinRK
|
r3949 | # instantiate | ||
MinRK
|
r3962 | self.subapp = subapp.instance() | ||
MinRK
|
r3949 | # and initialize subapp | ||
self.subapp.initialize(argv) | ||||
Brian Granger
|
r3790 | def parse_command_line(self, argv=None): | ||
"""Parse the command line arguments.""" | ||||
Brian Granger
|
r3795 | argv = sys.argv[1:] if argv is None else argv | ||
Brian Granger
|
r3790 | |||
MinRK
|
r3952 | if self.subcommands and len(argv) > 0: | ||
# we have subcommands, and one may have been specified | ||||
subc, subargv = argv[0], argv[1:] | ||||
MinRK
|
r3958 | if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands: | ||
MinRK
|
r3952 | # it's a subcommand, and *not* a flag or class parameter | ||
return self.initialize_subcommand(subc, subargv) | ||||
MinRK
|
r3949 | |||
MinRK
|
r3946 | if '-h' in argv or '--help' in argv or '--help-all' in argv: | ||
Brian Granger
|
r3790 | self.print_description() | ||
MinRK
|
r3946 | self.print_help('--help-all' in argv) | ||
MinRK
|
r3945 | self.exit(0) | ||
Brian Granger
|
r3790 | |||
if '--version' in argv: | ||||
self.print_version() | ||||
MinRK
|
r3945 | self.exit(0) | ||
MinRK
|
r3861 | |||
loader = KeyValueConfigLoader(argv=argv, aliases=self.aliases, | ||||
flags=self.flags) | ||||
MinRK
|
r3951 | try: | ||
config = loader.load_config() | ||||
except ArgumentError as e: | ||||
self.log.fatal(str(e)) | ||||
self.print_description() | ||||
self.print_help() | ||||
self.exit(1) | ||||
Brian Granger
|
r3790 | self.update_config(config) | ||
MinRK
|
r3958 | # store unparsed args in extra_args | ||
self.extra_args = loader.extra_args | ||||
Brian Granger
|
r3790 | |||
def load_config_file(self, filename, path=None): | ||||
"""Load a .py based config file by filename and path.""" | ||||
loader = PyFileConfigLoader(filename, path=path) | ||||
config = loader.load_config() | ||||
self.update_config(config) | ||||
Brian Granger
|
r3940 | def exit(self, exit_status=0): | ||
self.log.debug("Exiting application: %s" % self.name) | ||||
sys.exit(exit_status) | ||||
MinRK
|
r3944 | |||
#----------------------------------------------------------------------------- | ||||
# utility functions, for convenience | ||||
#----------------------------------------------------------------------------- | ||||
def boolean_flag(name, configurable, set_help='', unset_help=''): | ||||
"""helper for building basic --trait, --no-trait flags | ||||
Parameters | ||||
---------- | ||||
name : str | ||||
The name of the flag. | ||||
configurable : str | ||||
The 'Class.trait' string of the trait to be set/unset with the flag | ||||
set_help : unicode | ||||
help string for --name flag | ||||
unset_help : unicode | ||||
help string for --no-name flag | ||||
Returns | ||||
------- | ||||
cfg : dict | ||||
A dict with two keys: 'name', and 'no-name', for setting and unsetting | ||||
the trait, respectively. | ||||
""" | ||||
# default helpstrings | ||||
set_help = set_help or "set %s=True"%configurable | ||||
unset_help = unset_help or "set %s=False"%configurable | ||||
cls,trait = configurable.split('.') | ||||
MinRK
|
r3957 | setter = {cls : {trait : True}} | ||
unsetter = {cls : {trait : False}} | ||||
MinRK
|
r3944 | return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)} | ||