diff --git a/IPython/config/application.py b/IPython/config/application.py new file mode 100644 index 0000000..8468b33 --- /dev/null +++ b/IPython/config/application.py @@ -0,0 +1,520 @@ +# encoding: utf-8 +""" +A base class for a configurable application. + +Authors: + +* Brian Granger +* 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 logging +import os +import re +import sys +from copy import deepcopy +from collections import defaultdict + +from IPython.external.decorator import decorator + +from IPython.config.configurable import SingletonConfigurable +from IPython.config.loader import ( + KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, +) + +from IPython.utils.traitlets import ( + Unicode, List, Enum, Dict, Instance, TraitError +) +from IPython.utils.importstring import import_item +from IPython.utils.text import indent, wrap_paragraphs, dedent + +#----------------------------------------------------------------------------- +# function for re-wrapping a helpstring +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Descriptions for the various sections +#----------------------------------------------------------------------------- + +# merge flags&aliases into options +option_description = """ +Arguments that take values are actually convenience aliases to full +Configurables, whose aliases are listed on the help line. For more information +on full configurables, see '--help-all'. +""".strip() # trim newlines of front and back + +keyvalue_description = """ +Parameters are set from command-line arguments of the form: +`--Class.trait=value`. +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 + +subcommand_description = """ +Subcommands are launched as `{app} cmd [args]`. For information on using +subcommand 'cmd', do: `{app} cmd -h`. +""".strip().format(app=os.path.basename(sys.argv[0])) +# get running program name + +#----------------------------------------------------------------------------- +# Application class +#----------------------------------------------------------------------------- + +@decorator +def catch_config_error(method, app, *args, **kwargs): + """Method decorator for catching invalid config (Trait/ArgumentErrors) during init. + + On a TraitError (generally caused by bad config), this will print the trait's + message, and exit the app. + + For use on init methods, to prevent invoking excepthook on invalid input. + """ + try: + return method(app, *args, **kwargs) + except (TraitError, ArgumentError) as e: + app.print_description() + app.print_help() + app.print_examples() + app.log.fatal("Bad config encountered during initialization:") + app.log.fatal(str(e)) + app.log.debug("Config at the time: %s", app.config) + app.exit(1) + + +class ApplicationError(Exception): + pass + + +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 + 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.') + # default section descriptions + option_description = Unicode(option_description) + keyvalue_description = Unicode(keyvalue_description) + subcommand_description = Unicode(subcommand_description) + + # The usage and example string that goes at the end of the help string. + examples = Unicode() + + # A sequence of Configurable subclasses whose config=True attributes will + # be exposed at the command line. + 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,'DEBUG','INFO','WARN','ERROR','CRITICAL'), + default_value=logging.WARN, + config=True, + help="Set the log level by value or name.") + def _log_level_changed(self, name, old, new): + """Adjust the log level when log_level is set.""" + if isinstance(new, basestring): + new = getattr(logging, new) + self.log_level = new + self.log.setLevel(new) + + log_format = Unicode("[%(name)s] %(message)s", config=True, + help="The Logging format template", + ) + log = Instance(logging.Logger) + def _log_default(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. + """ + log = logging.getLogger(self.__class__.__name__) + log.setLevel(self.log_level) + if sys.executable.endswith('pythonw.exe'): + # this should really go to a file, but file-logging is only + # hooked up in parallel applications + _log_handler = logging.StreamHandler(open(os.devnull, 'w')) + else: + _log_handler = logging.StreamHandler() + _log_formatter = logging.Formatter(self.log_format) + _log_handler.setFormatter(_log_formatter) + log.addHandler(_log_handler) + return log + + # the alias map for configurables + aliases = Dict({'log-level' : 'Application.log_level'}) + + # 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() + def _flags_changed(self, name, old, new): + """ensure flags dict is valid""" + for key,value in new.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) + + + # 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) + + # extra command-line arguments that don't set config values + extra_args = List(Unicode) + + + def __init__(self, **kwargs): + SingletonConfigurable.__init__(self, **kwargs) + # Ensure my class is in self.classes, so my attributes appear in command line + # options and config files. + if self.__class__ not in self.classes: + self.classes.insert(0, self.__class__) + + def _config_changed(self, name, old, new): + SingletonConfigurable._config_changed(self, name, old, new) + self.log.debug('Config changed:') + self.log.debug(repr(new)) + + @catch_config_error + 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() + + def print_alias_help(self): + """Print the alias part of the help.""" + if not self.aliases: + return + + lines = [] + classdict = {} + for cls in self.classes: + # include all parents (up to, but excluding Configurable) in available names + for c in cls.mro()[:-3]: + classdict[c.__name__] = c + + for alias, longname in self.aliases.iteritems(): + classname, traitname = longname.split('.',1) + cls = classdict[classname] + + trait = cls.class_traits(config=True)[traitname] + help = cls.class_get_trait_help(trait).splitlines() + # reformat first line + help[0] = help[0].replace(longname, alias) + ' (%s)'%longname + if len(alias) == 1: + help[0] = help[0].replace('--%s='%alias, '-%s '%alias) + lines.extend(help) + # lines.append('') + print os.linesep.join(lines) + + def print_flag_help(self): + """Print the flag part of the help.""" + if not self.flags: + return + + lines = [] + for m, (cfg,help) in self.flags.iteritems(): + prefix = '--' if len(m) > 1 else '-' + lines.append(prefix+m) + lines.append(indent(dedent(help.strip()))) + # lines.append('') + print os.linesep.join(lines) + + def print_options(self): + if not self.flags and not self.aliases: + return + lines = ['Options'] + lines.append('-'*len(lines[0])) + lines.append('') + for p in wrap_paragraphs(self.option_description): + lines.append(p) + lines.append('') + print os.linesep.join(lines) + self.print_flag_help() + self.print_alias_help() + print + + def print_subcommands(self): + """Print the subcommand part of the help.""" + if not self.subcommands: + return + + lines = ["Subcommands"] + lines.append('-'*len(lines[0])) + lines.append('') + for p in wrap_paragraphs(self.subcommand_description): + lines.append(p) + lines.append('') + for subc, (cls, help) in self.subcommands.iteritems(): + lines.append(subc) + if help: + lines.append(indent(dedent(help.strip()))) + lines.append('') + print os.linesep.join(lines) + + 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. + """ + self.print_subcommands() + self.print_options() + + if classes: + if self.classes: + print "Class parameters" + print "----------------" + print + for p in wrap_paragraphs(self.keyvalue_description): + print p + print + + for cls in self.classes: + cls.class_print_help() + print + else: + print "To see all available configurables, use `--help-all`" + print + + def print_description(self): + """Print the application description.""" + for p in wrap_paragraphs(self.description): + print p + print + + def print_examples(self): + """Print usage and examples. + + This usage string goes at the end of the command line help string + and should contain examples of the application's usage. + """ + if self.examples: + print "Examples" + print "--------" + print + print indent(dedent(self.examples.strip())) + 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 = newconfig + + @catch_config_error + def initialize_subcommand(self, subc, argv=None): + """Initialize a subcommand with argv.""" + subapp,help = self.subcommands.get(subc) + + if isinstance(subapp, basestring): + subapp = import_item(subapp) + + # clear existing instances + self.__class__.clear_instance() + # instantiate + self.subapp = subapp.instance() + # and initialize subapp + self.subapp.initialize(argv) + + def flatten_flags(self): + """flatten flags and aliases, so cl-args override as expected. + + This prevents issues such as an alias pointing to InteractiveShell, + but a config file setting the same trait in TerminalInteraciveShell + getting inappropriate priority over the command-line arg. + + Only aliases with exactly one descendent in the class list + will be promoted. + + """ + # build a tree of classes in our list that inherit from a particular + # it will be a dict by parent classname of classes in our list + # that are descendents + mro_tree = defaultdict(list) + for cls in self.classes: + clsname = cls.__name__ + for parent in cls.mro()[1:-3]: + # exclude cls itself and Configurable,HasTraits,object + mro_tree[parent.__name__].append(clsname) + # flatten aliases, which have the form: + # { 'alias' : 'Class.trait' } + aliases = {} + for alias, cls_trait in self.aliases.iteritems(): + cls,trait = cls_trait.split('.',1) + children = mro_tree[cls] + if len(children) == 1: + # exactly one descendent, promote alias + cls = children[0] + aliases[alias] = '.'.join([cls,trait]) + + # flatten flags, which are of the form: + # { 'key' : ({'Cls' : {'trait' : value}}, 'help')} + flags = {} + for key, (flagdict, help) in self.flags.iteritems(): + newflag = {} + for cls, subdict in flagdict.iteritems(): + children = mro_tree[cls] + # exactly one descendent, promote flag section + if len(children) == 1: + cls = children[0] + newflag[cls] = subdict + flags[key] = (newflag, help) + return flags, aliases + + @catch_config_error + def parse_command_line(self, argv=None): + """Parse the command line arguments.""" + argv = sys.argv[1:] if argv is None else argv + + if argv and argv[0] == 'help': + # turn `ipython help notebook` into `ipython notebook -h` + argv = argv[1:] + ['-h'] + + if self.subcommands and len(argv) > 0: + # we have subcommands, and one may have been specified + subc, subargv = argv[0], argv[1:] + if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands: + # it's a subcommand, and *not* a flag or class parameter + return self.initialize_subcommand(subc, subargv) + + # Arguments after a '--' argument are for the script IPython may be + # about to run, not IPython iteslf. For arguments parsed here (help and + # version), we want to only search the arguments up to the first + # occurrence of '--', which we're calling interpreted_argv. + try: + interpreted_argv = argv[:argv.index('--')] + except ValueError: + interpreted_argv = argv + + if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')): + self.print_description() + self.print_help('--help-all' in interpreted_argv) + self.print_examples() + self.exit(0) + + if '--version' in interpreted_argv or '-V' in interpreted_argv: + self.print_version() + self.exit(0) + + # flatten flags&aliases, so cl-args get appropriate priority: + flags,aliases = self.flatten_flags() + + loader = KVArgParseConfigLoader(argv=argv, aliases=aliases, + flags=flags) + config = loader.load_config() + self.update_config(config) + # store unparsed args in extra_args + self.extra_args = loader.extra_args + + @catch_config_error + def load_config_file(self, filename, path=None): + """Load a .py based config file by filename and path.""" + loader = PyFileConfigLoader(filename, path=path) + try: + config = loader.load_config() + except ConfigFileNotFound: + # problem finding the file, raise + raise + except Exception: + # try to get the full filename, but it will be empty in the + # unlikely event that the error raised before filefind finished + filename = loader.full_filename or filename + # problem while running the file + self.log.error("Exception while loading config file %s", + filename, exc_info=True) + else: + self.log.debug("Loaded config file: %s", loader.full_filename) + self.update_config(config) + + def generate_config_file(self): + """generate default config file from Configurables""" + lines = ["# Configuration file for %s."%self.name] + lines.append('') + lines.append('c = get_config()') + lines.append('') + for cls in self.classes: + lines.append(cls.class_config_section()) + return '\n'.join(lines) + + def exit(self, exit_status=0): + self.log.debug("Exiting application: %s" % self.name) + sys.exit(exit_status) + +#----------------------------------------------------------------------------- +# 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('.') + + setter = {cls : {trait : True}} + unsetter = {cls : {trait : False}} + return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)} + diff --git a/IPython/config/loader.py b/IPython/config/loader.py new file mode 100644 index 0000000..12cf91f --- /dev/null +++ b/IPython/config/loader.py @@ -0,0 +1,701 @@ +"""A simple configuration system. + +Inheritance diagram: + +.. inheritance-diagram:: IPython.config.loader + :parts: 3 + +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 __builtin__ as builtin_mod +import os +import re +import sys + +from IPython.external import argparse +from IPython.utils.path import filefind, get_ipython_dir +from IPython.utils import py3compat, text, warn +from IPython.utils.encoding import DEFAULT_ENCODING + +#----------------------------------------------------------------------------- +# Exceptions +#----------------------------------------------------------------------------- + + +class ConfigError(Exception): + pass + +class ConfigLoaderError(ConfigError): + pass + +class ConfigFileNotFound(ConfigError): + pass + +class ArgumentError(ConfigLoaderError): + pass + +#----------------------------------------------------------------------------- +# Argparse fix +#----------------------------------------------------------------------------- + +# Unfortunately argparse by default prints help messages to stderr instead of +# stdout. This makes it annoying to capture long help screens at the command +# line, since one must know how to pipe stderr, which many users don't know how +# to do. So we override the print_help method with one that defaults to +# stdout and use our class instead. + +class ArgumentParser(argparse.ArgumentParser): + """Simple argparse subclass that prints help to stdout by default.""" + + def print_help(self, file=None): + if file is None: + file = sys.stdout + return super(ArgumentParser, self).print_help(file) + + print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__ + +#----------------------------------------------------------------------------- +# Config class for holding config information +#----------------------------------------------------------------------------- + + +class Config(dict): + """An attribute based dict that can do smart merges.""" + + def __init__(self, *args, **kwds): + dict.__init__(self, *args, **kwds) + # This sets self.__dict__ = self, but it has to be done this way + # because we are also overriding __setattr__. + dict.__setattr__(self, '__dict__', self) + + def _merge(self, other): + to_update = {} + for k, v in other.iteritems(): + if k not in self: + to_update[k] = v + else: # I have this key + if isinstance(v, Config): + # Recursively merge common sub Configs + self[k]._merge(v) + else: + # Plain updates for non-Configs + to_update[k] = v + + self.update(to_update) + + def _is_section_key(self, key): + if key[0].upper()==key[0] and not key.startswith('_'): + return True + else: + return False + + def __contains__(self, key): + if self._is_section_key(key): + return True + else: + return super(Config, self).__contains__(key) + # .has_key is deprecated for dictionaries. + has_key = __contains__ + + def _has_section(self, key): + if self._is_section_key(key): + if super(Config, self).__contains__(key): + return True + return False + + def copy(self): + return type(self)(dict.copy(self)) + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, memo): + import copy + return type(self)(copy.deepcopy(self.items())) + + def __getitem__(self, key): + # We cannot use directly self._is_section_key, because it triggers + # infinite recursion on top of PyPy. Instead, we manually fish the + # bound method. + is_section_key = self.__class__._is_section_key.__get__(self) + + # Because we use this for an exec namespace, we need to delegate + # the lookup of names in __builtin__ to itself. This means + # that you can't have section or attribute names that are + # builtins. + try: + return getattr(builtin_mod, key) + except AttributeError: + pass + if is_section_key(key): + try: + return dict.__getitem__(self, key) + except KeyError: + c = Config() + dict.__setitem__(self, key, c) + return c + else: + return dict.__getitem__(self, key) + + def __setitem__(self, key, value): + # Don't allow names in __builtin__ to be modified. + if hasattr(builtin_mod, key): + raise ConfigError('Config variable names cannot have the same name ' + 'as a Python builtin: %s' % key) + if self._is_section_key(key): + if not isinstance(value, Config): + raise ValueError('values whose keys begin with an uppercase ' + 'char must be Config instances: %r, %r' % (key, value)) + else: + dict.__setitem__(self, key, value) + + def __getattr__(self, key): + try: + return self.__getitem__(key) + except KeyError as e: + raise AttributeError(e) + + def __setattr__(self, key, value): + try: + self.__setitem__(key, value) + except KeyError as e: + raise AttributeError(e) + + def __delattr__(self, key): + try: + dict.__delitem__(self, key) + except KeyError as e: + raise AttributeError(e) + + +#----------------------------------------------------------------------------- +# Config loading classes +#----------------------------------------------------------------------------- + + +class ConfigLoader(object): + """A object for loading configurations from just about anywhere. + + The resulting configuration is packaged as a :class:`Struct`. + + Notes + ----- + A :class:`ConfigLoader` does one thing: load a config from a source + (file, command line arguments) and returns the data as a :class:`Struct`. + There are lots of things that :class:`ConfigLoader` does not do. It does + not implement complex logic for finding config files. It does not handle + default values or merge multiple configs. These things need to be + handled elsewhere. + """ + + def __init__(self): + """A base class for config loaders. + + Examples + -------- + + >>> cl = ConfigLoader() + >>> config = cl.load_config() + >>> config + {} + """ + self.clear() + + def clear(self): + self.config = Config() + + def load_config(self): + """Load a config from somewhere, return a :class:`Config` instance. + + Usually, this will cause self.config to be set and then returned. + However, in most cases, :meth:`ConfigLoader.clear` should be called + to erase any previous state. + """ + self.clear() + return self.config + + +class FileConfigLoader(ConfigLoader): + """A base class for file based configurations. + + As we add more file based config loaders, the common logic should go + here. + """ + pass + + +class PyFileConfigLoader(FileConfigLoader): + """A config loader for pure python files. + + This calls execfile on a plain python file and looks for attributes + that are all caps. These attribute are added to the config Struct. + """ + + def __init__(self, filename, path=None): + """Build a config loader for a filename and path. + + Parameters + ---------- + filename : str + The file name of the config file. + path : str, list, tuple + The path to search for the config file on, or a sequence of + paths to try in order. + """ + super(PyFileConfigLoader, self).__init__() + self.filename = filename + self.path = path + self.full_filename = '' + self.data = None + + def load_config(self): + """Load the config from a file and return it as a Struct.""" + self.clear() + try: + self._find_file() + except IOError as e: + raise ConfigFileNotFound(str(e)) + self._read_file_as_dict() + self._convert_to_config() + return self.config + + def _find_file(self): + """Try to find the file by searching the paths.""" + self.full_filename = filefind(self.filename, self.path) + + def _read_file_as_dict(self): + """Load the config file into self.config, with recursive loading.""" + # This closure is made available in the namespace that is used + # to exec the config file. It allows users to call + # load_subconfig('myconfig.py') to load config files recursively. + # It needs to be a closure because it has references to self.path + # and self.config. The sub-config is loaded with the same path + # as the parent, but it uses an empty config which is then merged + # with the parents. + + # If a profile is specified, the config file will be loaded + # from that profile + + def load_subconfig(fname, profile=None): + # import here to prevent circular imports + from IPython.core.profiledir import ProfileDir, ProfileDirError + if profile is not None: + try: + profile_dir = ProfileDir.find_profile_dir_by_name( + get_ipython_dir(), + profile, + ) + except ProfileDirError: + return + path = profile_dir.location + else: + path = self.path + loader = PyFileConfigLoader(fname, path) + try: + sub_config = loader.load_config() + except ConfigFileNotFound: + # Pass silently if the sub config is not there. This happens + # when a user s using a profile, but not the default config. + pass + else: + self.config._merge(sub_config) + + # Again, this needs to be a closure and should be used in config + # files to get the config being loaded. + def get_config(): + return self.config + + namespace = dict(load_subconfig=load_subconfig, get_config=get_config) + fs_encoding = sys.getfilesystemencoding() or 'ascii' + conf_filename = self.full_filename.encode(fs_encoding) + py3compat.execfile(conf_filename, namespace) + + def _convert_to_config(self): + if self.data is None: + ConfigLoaderError('self.data does not exist') + + +class CommandLineConfigLoader(ConfigLoader): + """A config loader for command line arguments. + + As we add more command line based loaders, the common logic should go + here. + """ + + def _exec_config_str(self, lhs, rhs): + """execute self.config. = + + * expands ~ with expanduser + * tries to assign with raw eval, otherwise assigns with just the string, + allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not* + equivalent are `--C.a=4` and `--C.a='4'`. + """ + rhs = os.path.expanduser(rhs) + try: + # Try to see if regular Python syntax will work. This + # won't handle strings as the quote marks are removed + # by the system shell. + value = eval(rhs) + except (NameError, SyntaxError): + # This case happens if the rhs is a string. + value = rhs + + exec u'self.config.%s = value' % lhs + + def _load_flag(self, cfg): + """update self.config from a flag, which can be a dict or Config""" + if isinstance(cfg, (dict, Config)): + # don't clobber whole config sections, update + # each section from config: + for sec,c in cfg.iteritems(): + self.config[sec].update(c) + else: + raise TypeError("Invalid flag: %r" % cfg) + +# raw --identifier=value pattern +# but *also* accept '-' as wordsep, for aliases +# accepts: --foo=a +# --Class.trait=value +# --alias-name=value +# rejects: -foo=value +# --foo +# --Class.trait +kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*') + +# just flags, no assignments, with two *or one* leading '-' +# accepts: --foo +# -foo-bar-again +# rejects: --anything=anything +# --two.word + +flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$') + +class KeyValueConfigLoader(CommandLineConfigLoader): + """A config loader that loads key value pairs from the command line. + + This allows command line options to be gives in the following form:: + + ipython --profile="foo" --InteractiveShell.autocall=False + """ + + def __init__(self, argv=None, aliases=None, flags=None): + """Create a key value pair config loader. + + Parameters + ---------- + argv : list + A list that has the form of sys.argv[1:] which has unicode + elements of the form u"key=value". If this is None (default), + then sys.argv[1:] will be used. + aliases : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'alias' : 'Configurable.trait'}` + flags : dict + A dict of flags, keyed by str name. Vaues can be Config objects, + dicts, or "key=value" strings. If Config or dict, when the flag + is triggered, The flag is loaded as `self.config.update(m)`. + + Returns + ------- + config : Config + The resulting Config object. + + Examples + -------- + + >>> from IPython.config.loader import KeyValueConfigLoader + >>> cl = KeyValueConfigLoader() + >>> d = cl.load_config(["--A.name='brian'","--B.number=0"]) + >>> sorted(d.items()) + [('A', {'name': 'brian'}), ('B', {'number': 0})] + """ + self.clear() + if argv is None: + argv = sys.argv[1:] + self.argv = argv + self.aliases = aliases or {} + self.flags = flags or {} + + + def clear(self): + super(KeyValueConfigLoader, self).clear() + self.extra_args = [] + + + def _decode_argv(self, argv, enc=None): + """decode argv if bytes, using stin.encoding, falling back on default enc""" + uargv = [] + if enc is None: + enc = DEFAULT_ENCODING + for arg in argv: + if not isinstance(arg, unicode): + # only decode if not already decoded + arg = arg.decode(enc) + uargv.append(arg) + return uargv + + + def load_config(self, argv=None, aliases=None, flags=None): + """Parse the configuration and generate the Config object. + + After loading, any arguments that are not key-value or + flags will be stored in self.extra_args - a list of + unparsed command-line arguments. This is used for + arguments such as input files or subcommands. + + Parameters + ---------- + argv : list, optional + A list that has the form of sys.argv[1:] which has unicode + elements of the form u"key=value". If this is None (default), + then self.argv will be used. + aliases : dict + A dict of aliases for configurable traits. + Keys are the short aliases, Values are the resolved trait. + Of the form: `{'alias' : 'Configurable.trait'}` + flags : dict + A dict of flags, keyed by str name. Values can be Config objects + or dicts. When the flag is triggered, The config is loaded as + `self.config.update(cfg)`. + """ + from IPython.config.configurable import Configurable + + self.clear() + if argv is None: + argv = self.argv + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + + # ensure argv is a list of unicode strings: + uargv = self._decode_argv(argv) + for idx,raw in enumerate(uargv): + # strip leading '-' + item = raw.lstrip('-') + + if raw == '--': + # don't parse arguments after '--' + # this is useful for relaying arguments to scripts, e.g. + # ipython -i foo.py --pylab=qt -- args after '--' go-to-foo.py + self.extra_args.extend(uargv[idx+1:]) + break + + if kv_pattern.match(raw): + lhs,rhs = item.split('=',1) + # Substitute longnames for aliases. + if lhs in aliases: + lhs = aliases[lhs] + if '.' not in lhs: + # probably a mistyped alias, but not technically illegal + warn.warn("Unrecognized alias: '%s', it will probably have no effect."%lhs) + try: + self._exec_config_str(lhs, rhs) + except Exception: + raise ArgumentError("Invalid argument: '%s'" % raw) + + elif flag_pattern.match(raw): + if item in flags: + cfg,help = flags[item] + self._load_flag(cfg) + else: + raise ArgumentError("Unrecognized flag: '%s'"%raw) + elif raw.startswith('-'): + kv = '--'+item + if kv_pattern.match(kv): + raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv)) + else: + raise ArgumentError("Invalid argument: '%s'"%raw) + else: + # keep all args that aren't valid in a list, + # in case our parent knows what to do with them. + self.extra_args.append(item) + return self.config + +class ArgParseConfigLoader(CommandLineConfigLoader): + """A loader that uses the argparse module to load from the command line.""" + + def __init__(self, argv=None, aliases=None, flags=None, *parser_args, **parser_kw): + """Create a config loader for use with argparse. + + Parameters + ---------- + + argv : optional, list + If given, used to read command-line arguments from, otherwise + sys.argv[1:] is used. + + parser_args : tuple + A tuple of positional arguments that will be passed to the + constructor of :class:`argparse.ArgumentParser`. + + parser_kw : dict + A tuple of keyword arguments that will be passed to the + constructor of :class:`argparse.ArgumentParser`. + + Returns + ------- + config : Config + The resulting Config object. + """ + super(CommandLineConfigLoader, self).__init__() + self.clear() + if argv is None: + argv = sys.argv[1:] + self.argv = argv + self.aliases = aliases or {} + self.flags = flags or {} + + self.parser_args = parser_args + self.version = parser_kw.pop("version", None) + kwargs = dict(argument_default=argparse.SUPPRESS) + kwargs.update(parser_kw) + self.parser_kw = kwargs + + def load_config(self, argv=None, aliases=None, flags=None): + """Parse command line arguments and return as a Config object. + + Parameters + ---------- + + args : optional, list + If given, a list with the structure of sys.argv[1:] to parse + arguments from. If not given, the instance's self.argv attribute + (given at construction time) is used.""" + self.clear() + if argv is None: + argv = self.argv + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + self._create_parser(aliases, flags) + self._parse_args(argv) + self._convert_to_config() + return self.config + + def get_extra_args(self): + if hasattr(self, 'extra_args'): + return self.extra_args + else: + return [] + + def _create_parser(self, aliases=None, flags=None): + self.parser = ArgumentParser(*self.parser_args, **self.parser_kw) + self._add_arguments(aliases, flags) + + def _add_arguments(self, aliases=None, flags=None): + raise NotImplementedError("subclasses must implement _add_arguments") + + def _parse_args(self, args): + """self.parser->self.parsed_data""" + # decode sys.argv to support unicode command-line options + enc = DEFAULT_ENCODING + uargs = [py3compat.cast_unicode(a, enc) for a in args] + self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs) + + def _convert_to_config(self): + """self.parsed_data->self.config""" + for k, v in vars(self.parsed_data).iteritems(): + exec "self.config.%s = v"%k in locals(), globals() + +class KVArgParseConfigLoader(ArgParseConfigLoader): + """A config loader that loads aliases and flags with argparse, + but will use KVLoader for the rest. This allows better parsing + of common args, such as `ipython -c 'print 5'`, but still gets + arbitrary config with `ipython --InteractiveShell.use_readline=False`""" + + def _add_arguments(self, aliases=None, flags=None): + self.alias_flags = {} + # print aliases, flags + if aliases is None: + aliases = self.aliases + if flags is None: + flags = self.flags + paa = self.parser.add_argument + for key,value in aliases.iteritems(): + if key in flags: + # flags + nargs = '?' + else: + nargs = None + if len(key) is 1: + paa('-'+key, '--'+key, type=unicode, dest=value, nargs=nargs) + else: + paa('--'+key, type=unicode, dest=value, nargs=nargs) + for key, (value, help) in flags.iteritems(): + if key in self.aliases: + # + self.alias_flags[self.aliases[key]] = value + continue + if len(key) is 1: + paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value) + else: + paa('--'+key, action='append_const', dest='_flags', const=value) + + def _convert_to_config(self): + """self.parsed_data->self.config, parse unrecognized extra args via KVLoader.""" + # remove subconfigs list from namespace before transforming the Namespace + if '_flags' in self.parsed_data: + subcs = self.parsed_data._flags + del self.parsed_data._flags + else: + subcs = [] + + for k, v in vars(self.parsed_data).iteritems(): + if v is None: + # it was a flag that shares the name of an alias + subcs.append(self.alias_flags[k]) + else: + # eval the KV assignment + self._exec_config_str(k, v) + + for subc in subcs: + self._load_flag(subc) + + if self.extra_args: + sub_parser = KeyValueConfigLoader() + sub_parser.load_config(self.extra_args) + self.config._merge(sub_parser.config) + self.extra_args = sub_parser.extra_args + + +def load_pyconfig_files(config_files, path): + """Load multiple Python config files, merging each of them in turn. + + Parameters + ========== + config_files : list of str + List of config files names to load and merge into the config. + path : unicode + The full path to the location of the config files. + """ + config = Config() + for cf in config_files: + loader = PyFileConfigLoader(cf, path=path) + try: + next_config = loader.load_config() + except ConfigFileNotFound: + pass + except: + raise + else: + config._merge(next_config) + return config