Uploaded Test files
This commit is contained in:
parent
f584ad9d97
commit
2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions
8
venv/Lib/site-packages/traitlets/config/__init__.py
Normal file
8
venv/Lib/site-packages/traitlets/config/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
# encoding: utf-8
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from .application import *
|
||||
from .configurable import *
|
||||
from .loader import Config
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
711
venv/Lib/site-packages/traitlets/config/application.py
Normal file
711
venv/Lib/site-packages/traitlets/config/application.py
Normal file
|
@ -0,0 +1,711 @@
|
|||
# encoding: utf-8
|
||||
"""A base class for a configurable application."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from decorator import decorator
|
||||
|
||||
from traitlets.config.configurable import Configurable, SingletonConfigurable
|
||||
from traitlets.config.loader import (
|
||||
KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
|
||||
)
|
||||
|
||||
from traitlets.traitlets import (
|
||||
Bool, Unicode, List, Enum, Dict, Instance, TraitError, observe, observe_compat, default,
|
||||
)
|
||||
from ipython_genutils.importstring import import_item
|
||||
from ipython_genutils.text import indent, wrap_paragraphs, dedent
|
||||
from ipython_genutils import py3compat
|
||||
|
||||
import six
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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
|
||||
|
||||
# sys.argv can be missing, for example when python is embedded. See the docs
|
||||
# for details: http://docs.python.org/2/c-api/intro.html#embedding-python
|
||||
if not hasattr(sys, "argv"):
|
||||
sys.argv = [""]
|
||||
|
||||
subcommand_description = """
|
||||
Subcommands are launched as `{app} cmd [args]`. For information on using
|
||||
subcommand 'cmd', do: `{app} cmd -h`.
|
||||
"""
|
||||
# get running program name
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Application class
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
_envvar = os.environ.get('TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR','')
|
||||
if _envvar.lower() in {'1','true'}:
|
||||
TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = True
|
||||
elif _envvar.lower() in {'0','false',''} :
|
||||
TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = False
|
||||
else:
|
||||
raise ValueError("Unsupported value for environment variable: 'TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."% _envvar )
|
||||
|
||||
|
||||
@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_help()
|
||||
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 LevelFormatter(logging.Formatter):
|
||||
"""Formatter with additional `highlevel` record
|
||||
|
||||
This field is empty if log level is less than highlevel_limit,
|
||||
otherwise it is formatted with self.highlevel_format.
|
||||
|
||||
Useful for adding 'WARNING' to warning messages,
|
||||
without adding 'INFO' to info, etc.
|
||||
"""
|
||||
highlevel_limit = logging.WARN
|
||||
highlevel_format = " %(levelname)s |"
|
||||
|
||||
def format(self, record):
|
||||
if record.levelno >= self.highlevel_limit:
|
||||
record.highlevel = self.highlevel_format % record.__dict__
|
||||
else:
|
||||
record.highlevel = ""
|
||||
return super(LevelFormatter, self).format(record)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
python_config_loader_class = PyFileConfigLoader
|
||||
json_config_loader_class = JSONFileConfigLoader
|
||||
|
||||
# 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 = []
|
||||
|
||||
def _classes_inc_parents(self):
|
||||
"""Iterate through configurable classes, including configurable parents
|
||||
|
||||
Children should always be after parents, and each class should only be
|
||||
yielded once.
|
||||
"""
|
||||
seen = set()
|
||||
for c in self.classes:
|
||||
# We want to sort parents before children, so we reverse the MRO
|
||||
for parent in reversed(c.mro()):
|
||||
if issubclass(parent, Configurable) and (parent not in seen):
|
||||
seen.add(parent)
|
||||
yield parent
|
||||
|
||||
# The version string of this application.
|
||||
version = Unicode(u'0.0')
|
||||
|
||||
# the argv used to initialize the application
|
||||
argv = List()
|
||||
|
||||
# Whether failing to load config files should prevent startup
|
||||
raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR)
|
||||
|
||||
# The log level for the application
|
||||
log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
|
||||
default_value=logging.WARN,
|
||||
help="Set the log level by value or name.").tag(config=True)
|
||||
|
||||
@observe('log_level')
|
||||
@observe_compat
|
||||
def _log_level_changed(self, change):
|
||||
"""Adjust the log level when log_level is set."""
|
||||
new = change.new
|
||||
if isinstance(new, six.string_types):
|
||||
new = getattr(logging, new)
|
||||
self.log_level = new
|
||||
self.log.setLevel(new)
|
||||
|
||||
_log_formatter_cls = LevelFormatter
|
||||
|
||||
log_datefmt = Unicode("%Y-%m-%d %H:%M:%S",
|
||||
help="The date format used by logging formatters for %(asctime)s"
|
||||
).tag(config=True)
|
||||
|
||||
log_format = Unicode("[%(name)s]%(highlevel)s %(message)s",
|
||||
help="The Logging format template",
|
||||
).tag(config=True)
|
||||
|
||||
@observe('log_datefmt', 'log_format')
|
||||
@observe_compat
|
||||
def _log_format_changed(self, change):
|
||||
"""Change the log formatter when log_format is set."""
|
||||
_log_handler = self.log.handlers[0]
|
||||
_log_formatter = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
|
||||
_log_handler.setFormatter(_log_formatter)
|
||||
|
||||
@default('log')
|
||||
def _log_default(self):
|
||||
"""Start logging for this application.
|
||||
|
||||
The default is to log to stderr using a StreamHandler, if no default
|
||||
handler already exists. The log level starts at logging.WARN, but this
|
||||
can be adjusted by setting the ``log_level`` attribute.
|
||||
"""
|
||||
log = logging.getLogger(self.__class__.__name__)
|
||||
log.setLevel(self.log_level)
|
||||
log.propagate = False
|
||||
_log = log # copied from Logger.hasHandlers() (new in Python 3.2)
|
||||
while _log:
|
||||
if _log.handlers:
|
||||
return log
|
||||
if not _log.propagate:
|
||||
break
|
||||
else:
|
||||
_log = _log.parent
|
||||
if sys.executable and 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 = self._log_formatter_cls(fmt=self.log_format, datefmt=self.log_datefmt)
|
||||
_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()
|
||||
@observe('flags')
|
||||
@observe_compat
|
||||
def _flags_changed(self, change):
|
||||
"""ensure flags dict is valid"""
|
||||
new = change.new
|
||||
for key, value in new.items():
|
||||
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], six.string_types), "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('traitlets.config.application.Application', allow_none=True)
|
||||
|
||||
# extra command-line arguments that don't set config values
|
||||
extra_args = List(Unicode())
|
||||
|
||||
cli_config = Instance(Config, (), {},
|
||||
help="""The subset of our configuration that came from the command-line
|
||||
|
||||
We re-load this configuration after loading config files,
|
||||
to ensure that it maintains highest priority.
|
||||
"""
|
||||
)
|
||||
|
||||
_loaded_config_files = List()
|
||||
|
||||
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.
|
||||
cls = self.__class__
|
||||
if cls not in self.classes:
|
||||
if self.classes is cls.classes:
|
||||
# class attr, assign instead of insert
|
||||
cls.classes = [cls] + self.classes
|
||||
else:
|
||||
self.classes.insert(0, self.__class__)
|
||||
|
||||
@observe('config')
|
||||
@observe_compat
|
||||
def _config_changed(self, change):
|
||||
super(Application, self)._config_changed(change)
|
||||
self.log.debug('Config changed:')
|
||||
self.log.debug(repr(change.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.items():
|
||||
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.items():
|
||||
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.format(
|
||||
app=self.name)):
|
||||
lines.append(p)
|
||||
lines.append('')
|
||||
for subc, (cls, help) in self.subcommands.items():
|
||||
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_description()
|
||||
self.print_subcommands()
|
||||
self.print_options()
|
||||
|
||||
if classes:
|
||||
help_classes = self.classes
|
||||
if help_classes:
|
||||
print("Class parameters")
|
||||
print("----------------")
|
||||
print()
|
||||
for p in wrap_paragraphs(self.keyvalue_description):
|
||||
print(p)
|
||||
print()
|
||||
|
||||
for cls in help_classes:
|
||||
cls.class_print_help()
|
||||
print()
|
||||
else:
|
||||
print("To see all available configurables, use `--help-all`")
|
||||
print()
|
||||
|
||||
self.print_examples()
|
||||
|
||||
def document_config_options(self):
|
||||
"""Generate rST format documentation for the config options this application
|
||||
|
||||
Returns a multiline string.
|
||||
"""
|
||||
return '\n'.join(c.class_config_rst_doc()
|
||||
for c in self._classes_inc_parents())
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@catch_config_error
|
||||
def initialize_subcommand(self, subc, argv=None):
|
||||
"""Initialize a subcommand with argv."""
|
||||
subapp,help = self.subcommands.get(subc)
|
||||
|
||||
if isinstance(subapp, six.string_types):
|
||||
subapp = import_item(subapp)
|
||||
|
||||
# clear existing instances
|
||||
self.__class__.clear_instance()
|
||||
# instantiate
|
||||
self.subapp = subapp.instance(parent=self)
|
||||
# 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.items():
|
||||
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.items():
|
||||
newflag = {}
|
||||
for cls, subdict in flagdict.items():
|
||||
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
|
||||
self.argv = [ py3compat.cast_unicode(arg) for arg in 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_help('--help-all' in interpreted_argv)
|
||||
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, log=self.log)
|
||||
self.cli_config = deepcopy(loader.load_config())
|
||||
self.update_config(self.cli_config)
|
||||
# store unparsed args in extra_args
|
||||
self.extra_args = loader.extra_args
|
||||
|
||||
@classmethod
|
||||
def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file_errors=False):
|
||||
"""Load config files (py,json) by filename and path.
|
||||
|
||||
yield each config object in turn.
|
||||
"""
|
||||
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
for path in path[::-1]:
|
||||
# path list is in descending priority order, so load files backwards:
|
||||
pyloader = cls.python_config_loader_class(basefilename+'.py', path=path, log=log)
|
||||
if log:
|
||||
log.debug("Looking for %s in %s", basefilename, path or os.getcwd())
|
||||
jsonloader = cls.json_config_loader_class(basefilename+'.json', path=path, log=log)
|
||||
loaded = []
|
||||
filenames = []
|
||||
for loader in [pyloader, jsonloader]:
|
||||
config = None
|
||||
try:
|
||||
config = loader.load_config()
|
||||
except ConfigFileNotFound:
|
||||
pass
|
||||
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 basefilename
|
||||
# problem while running the file
|
||||
if raise_config_file_errors:
|
||||
raise
|
||||
if log:
|
||||
log.error("Exception while loading config file %s",
|
||||
filename, exc_info=True)
|
||||
else:
|
||||
if log:
|
||||
log.debug("Loaded config file: %s", loader.full_filename)
|
||||
if config:
|
||||
for filename, earlier_config in zip(filenames, loaded):
|
||||
collisions = earlier_config.collisions(config)
|
||||
if collisions and log:
|
||||
log.warning("Collisions detected in {0} and {1} config files."
|
||||
" {1} has higher priority: {2}".format(
|
||||
filename, loader.full_filename, json.dumps(collisions, indent=2),
|
||||
))
|
||||
yield (config, loader.full_filename)
|
||||
loaded.append(config)
|
||||
filenames.append(loader.full_filename)
|
||||
|
||||
@property
|
||||
def loaded_config_files(self):
|
||||
"""Currently loaded configuration files"""
|
||||
return self._loaded_config_files[:]
|
||||
|
||||
@catch_config_error
|
||||
def load_config_file(self, filename, path=None):
|
||||
"""Load config files by filename and path."""
|
||||
filename, ext = os.path.splitext(filename)
|
||||
new_config = Config()
|
||||
for (config, filename) in self._load_config_files(filename, path=path, log=self.log,
|
||||
raise_config_file_errors=self.raise_config_file_errors,
|
||||
):
|
||||
new_config.merge(config)
|
||||
if filename not in self._loaded_config_files: # only add to list of loaded files if not previously loaded
|
||||
self._loaded_config_files.append(filename)
|
||||
# add self.cli_config to preserve CLI config priority
|
||||
new_config.merge(self.cli_config)
|
||||
self.update_config(new_config)
|
||||
|
||||
|
||||
def _classes_in_config_sample(self):
|
||||
"""
|
||||
Yields only classes with own traits, and their subclasses.
|
||||
|
||||
Thus, produced sample config-file will contain all classes
|
||||
on which a trait-value may be overridden:
|
||||
|
||||
- either on the class owning the trait,
|
||||
- or on its subclasses, even if those subclasses do not define
|
||||
any traits themselves.
|
||||
"""
|
||||
cls_to_config = OrderedDict( (cls, bool(cls.class_own_traits(config=True)))
|
||||
for cls
|
||||
in self._classes_inc_parents())
|
||||
|
||||
def is_any_parent_included(cls):
|
||||
return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__)
|
||||
|
||||
## Mark "empty" classes for inclusion if their parents own-traits,
|
||||
# and loop until no more classes gets marked.
|
||||
#
|
||||
while True:
|
||||
to_incl_orig = cls_to_config.copy()
|
||||
cls_to_config = OrderedDict( (cls, inc_yes or is_any_parent_included(cls))
|
||||
for cls, inc_yes
|
||||
in cls_to_config.items())
|
||||
if cls_to_config == to_incl_orig:
|
||||
break
|
||||
for cl, inc_yes in cls_to_config.items():
|
||||
if inc_yes:
|
||||
yield cl
|
||||
|
||||
def generate_config_file(self):
|
||||
"""generate default config file from Configurables"""
|
||||
lines = ["# Configuration file for %s." % self.name]
|
||||
lines.append('')
|
||||
for cls in self._classes_in_config_sample():
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def launch_instance(cls, argv=None, **kwargs):
|
||||
"""Launch a global instance of this Application
|
||||
|
||||
If a global instance already exists, this reinitializes and starts it
|
||||
"""
|
||||
app = cls.instance(**kwargs)
|
||||
app.initialize(argv)
|
||||
app.start()
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)}
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Get the config object for the global Application instance, if there is one
|
||||
|
||||
otherwise return an empty config object
|
||||
"""
|
||||
if Application.initialized():
|
||||
return Application.instance().config
|
||||
else:
|
||||
return Config()
|
432
venv/Lib/site-packages/traitlets/config/configurable.py
Normal file
432
venv/Lib/site-packages/traitlets/config/configurable.py
Normal file
|
@ -0,0 +1,432 @@
|
|||
# encoding: utf-8
|
||||
"""A base class for objects that are configurable."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from copy import deepcopy
|
||||
import warnings
|
||||
|
||||
from .loader import Config, LazyConfigValue, _is_section_key
|
||||
from traitlets.traitlets import HasTraits, Instance, observe, observe_compat, default
|
||||
from ipython_genutils.text import indent, dedent, wrap_paragraphs
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Helper classes for Configurables
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ConfigurableError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MultipleInstanceError(ConfigurableError):
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Configurable implementation
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class Configurable(HasTraits):
|
||||
|
||||
config = Instance(Config, (), {})
|
||||
parent = Instance('traitlets.config.configurable.Configurable', allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Create a configurable given a config config.
|
||||
|
||||
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.
|
||||
parent : Configurable instance, optional
|
||||
The parent Configurable instance of this object.
|
||||
|
||||
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=config)
|
||||
# Then any other code you need to finish initialization.
|
||||
|
||||
This ensures that instances will be configured properly.
|
||||
"""
|
||||
parent = kwargs.pop('parent', None)
|
||||
if parent is not None:
|
||||
# config is implied from parent
|
||||
if kwargs.get('config', None) is None:
|
||||
kwargs['config'] = parent.config
|
||||
self.parent = parent
|
||||
|
||||
config = kwargs.pop('config', None)
|
||||
|
||||
# load kwarg traits, other than config
|
||||
super(Configurable, self).__init__(**kwargs)
|
||||
|
||||
# load config
|
||||
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
|
||||
else:
|
||||
# allow _config_default to return something
|
||||
self._load_config(self.config)
|
||||
|
||||
# Ensure explicit kwargs are applied after loading config.
|
||||
# This is usually redundant, but ensures config doesn't override
|
||||
# explicitly assigned values.
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Static trait notifiations
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
@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)
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
my_config = self._find_my_config(cfg)
|
||||
|
||||
# hold trait notifications until after all config has been loaded
|
||||
with self.hold_trait_notifications():
|
||||
for name, config_value in my_config.items():
|
||||
if name in traits:
|
||||
if isinstance(config_value, LazyConfigValue):
|
||||
# ConfigValue is a wrapper for using append / update on containers
|
||||
# without having to copy the initial value
|
||||
initial = getattr(self, name)
|
||||
config_value = config_value.get_value(initial)
|
||||
# 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))
|
||||
elif not _is_section_key(name) and not isinstance(config_value, Config):
|
||||
from difflib import get_close_matches
|
||||
if isinstance(self, LoggingConfigurable):
|
||||
warn = self.log.warning
|
||||
else:
|
||||
warn = lambda msg: warnings.warn(msg, stacklevel=9)
|
||||
matches = get_close_matches(name, traits)
|
||||
msg = u"Config option `{option}` not recognized by `{klass}`.".format(
|
||||
option=name, klass=self.__class__.__name__)
|
||||
|
||||
if len(matches) == 1:
|
||||
msg += u" Did you mean `{matches}`?".format(matches=matches[0])
|
||||
elif len(matches) >= 1:
|
||||
msg +=" Did you mean one of: `{matches}`?".format(matches=', '.join(sorted(matches)))
|
||||
warn(msg)
|
||||
|
||||
@observe('config')
|
||||
@observe_compat
|
||||
def _config_changed(self, change):
|
||||
"""Update all the class traits having ``config=True`` in 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(change.new, traits=traits, section_names=section_names)
|
||||
|
||||
def update_config(self, config):
|
||||
"""Update config and load the new values"""
|
||||
# traitlets prior to 4.2 created a copy of self.config in order to trigger change events.
|
||||
# Some projects (IPython < 5) relied upon one side effect of this,
|
||||
# that self.config prior to update_config was not modified in-place.
|
||||
# For backward-compatibility, we must ensure that self.config
|
||||
# is a new object and not modified in-place,
|
||||
# but config consumers should not rely on this behavior.
|
||||
self.config = deepcopy(self.config)
|
||||
# load config
|
||||
self._load_config(config)
|
||||
# merge it into self.config
|
||||
self.config.merge(config)
|
||||
# TODO: trigger change event if/when dict-update change events take place
|
||||
# DO NOT trigger full trait-change
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
final_help = []
|
||||
final_help.append(u'%s options' % cls.__name__)
|
||||
final_help.append(len(final_help[0])*u'-')
|
||||
for k, v in sorted(cls.class_traits(config=True).items()):
|
||||
help = cls.class_get_trait_help(v, inst)
|
||||
final_help.append(help)
|
||||
return '\n'.join(final_help)
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
lines = []
|
||||
header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
|
||||
lines.append(header)
|
||||
if inst is not None:
|
||||
lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
|
||||
else:
|
||||
try:
|
||||
dvr = trait.default_value_repr()
|
||||
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))
|
||||
if 'Enum' in trait.__class__.__name__:
|
||||
# include Enum choices
|
||||
lines.append(indent('Choices: %r' % (trait.values,)))
|
||||
|
||||
help = trait.help
|
||||
if help != '':
|
||||
help = '\n'.join(wrap_paragraphs(help, 76))
|
||||
lines.append(indent(help, 4))
|
||||
return '\n'.join(lines)
|
||||
|
||||
@classmethod
|
||||
def class_print_help(cls, inst=None):
|
||||
"""Get the help string for a single trait and print it."""
|
||||
print(cls.class_get_help(inst))
|
||||
|
||||
@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))
|
||||
|
||||
return '## ' + s.replace('\n', '\n# ')
|
||||
|
||||
# section header
|
||||
breaker = '#' + '-'*78
|
||||
parent_classes = ','.join(p.__name__ for p in cls.__bases__)
|
||||
s = "# %s(%s) configuration" % (cls.__name__, parent_classes)
|
||||
lines = [breaker, s, breaker, '']
|
||||
# get the description trait
|
||||
desc = cls.class_traits().get('description')
|
||||
if desc:
|
||||
desc = desc.default_value
|
||||
if not desc:
|
||||
# no description from trait, use __doc__
|
||||
desc = getattr(cls, '__doc__', '')
|
||||
if desc:
|
||||
lines.append(c(desc))
|
||||
lines.append('')
|
||||
|
||||
for name, trait in sorted(cls.class_own_traits(config=True).items()):
|
||||
lines.append(c(trait.help))
|
||||
lines.append('#c.%s.%s = %s' % (cls.__name__, name, trait.default_value_repr()))
|
||||
lines.append('')
|
||||
return '\n'.join(lines)
|
||||
|
||||
@classmethod
|
||||
def class_config_rst_doc(cls):
|
||||
"""Generate rST documentation for this class' config options.
|
||||
|
||||
Excludes traits defined on parent classes.
|
||||
"""
|
||||
lines = []
|
||||
classname = cls.__name__
|
||||
for k, trait in sorted(cls.class_own_traits(config=True).items()):
|
||||
ttype = trait.__class__.__name__
|
||||
|
||||
termline = classname + '.' + trait.name
|
||||
|
||||
# Choices or type
|
||||
if 'Enum' in ttype:
|
||||
# include Enum choices
|
||||
termline += ' : ' + '|'.join(repr(x) for x in trait.values)
|
||||
else:
|
||||
termline += ' : ' + ttype
|
||||
lines.append(termline)
|
||||
|
||||
# Default value
|
||||
try:
|
||||
dvr = trait.default_value_repr()
|
||||
except Exception:
|
||||
dvr = None # ignore defaults we can't construct
|
||||
if dvr is not None:
|
||||
if len(dvr) > 64:
|
||||
dvr = dvr[:61]+'...'
|
||||
# Double up backslashes, so they get to the rendered docs
|
||||
dvr = dvr.replace('\\n', '\\\\n')
|
||||
lines.append(' Default: ``%s``' % dvr)
|
||||
lines.append('')
|
||||
|
||||
help = trait.help or 'No description'
|
||||
lines.append(indent(dedent(help), 4))
|
||||
|
||||
# Blank line
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
log = Instance('logging.Logger')
|
||||
@default('log')
|
||||
def _log_default(self):
|
||||
from traitlets import log
|
||||
return log.get_logger()
|
||||
|
||||
|
||||
class SingletonConfigurable(LoggingConfigurable):
|
||||
"""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 _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
|
||||
|
||||
@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 traitlets.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
|
||||
# parent classes' _instance attribute.
|
||||
for subclass in cls._walk_mro():
|
||||
subclass._instance = inst
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
857
venv/Lib/site-packages/traitlets/config/loader.py
Normal file
857
venv/Lib/site-packages/traitlets/config/loader.py
Normal file
|
@ -0,0 +1,857 @@
|
|||
# encoding: utf-8
|
||||
"""A simple configuration system."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
from ast import literal_eval
|
||||
|
||||
from ipython_genutils.path import filefind
|
||||
from ipython_genutils import py3compat
|
||||
from ipython_genutils.encoding import DEFAULT_ENCODING
|
||||
from six import text_type
|
||||
from traitlets.traitlets import HasTraits, List, Any
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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 LazyConfigValue(HasTraits):
|
||||
"""Proxy object for exposing methods on configurable containers
|
||||
|
||||
Exposes:
|
||||
|
||||
- append, extend, insert on lists
|
||||
- update on dicts
|
||||
- update, add on sets
|
||||
"""
|
||||
|
||||
_value = None
|
||||
|
||||
# list methods
|
||||
_extend = List()
|
||||
_prepend = List()
|
||||
|
||||
def append(self, obj):
|
||||
self._extend.append(obj)
|
||||
|
||||
def extend(self, other):
|
||||
self._extend.extend(other)
|
||||
|
||||
def prepend(self, other):
|
||||
"""like list.extend, but for the front"""
|
||||
self._prepend[:0] = other
|
||||
|
||||
_inserts = List()
|
||||
def insert(self, index, other):
|
||||
if not isinstance(index, int):
|
||||
raise TypeError("An integer is required")
|
||||
self._inserts.append((index, other))
|
||||
|
||||
# dict methods
|
||||
# update is used for both dict and set
|
||||
_update = Any()
|
||||
def update(self, other):
|
||||
if self._update is None:
|
||||
if isinstance(other, dict):
|
||||
self._update = {}
|
||||
else:
|
||||
self._update = set()
|
||||
self._update.update(other)
|
||||
|
||||
# set methods
|
||||
def add(self, obj):
|
||||
self.update({obj})
|
||||
|
||||
def get_value(self, initial):
|
||||
"""construct the value from the initial one
|
||||
|
||||
after applying any insert / extend / update changes
|
||||
"""
|
||||
if self._value is not None:
|
||||
return self._value
|
||||
value = copy.deepcopy(initial)
|
||||
if isinstance(value, list):
|
||||
for idx, obj in self._inserts:
|
||||
value.insert(idx, obj)
|
||||
value[:0] = self._prepend
|
||||
value.extend(self._extend)
|
||||
|
||||
elif isinstance(value, dict):
|
||||
if self._update:
|
||||
value.update(self._update)
|
||||
elif isinstance(value, set):
|
||||
if self._update:
|
||||
value.update(self._update)
|
||||
self._value = value
|
||||
return value
|
||||
|
||||
def to_dict(self):
|
||||
"""return JSONable dict form of my data
|
||||
|
||||
Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
|
||||
"""
|
||||
d = {}
|
||||
if self._update:
|
||||
d['update'] = self._update
|
||||
if self._extend:
|
||||
d['extend'] = self._extend
|
||||
if self._prepend:
|
||||
d['prepend'] = self._prepend
|
||||
elif self._inserts:
|
||||
d['inserts'] = self._inserts
|
||||
return d
|
||||
|
||||
|
||||
def _is_section_key(key):
|
||||
"""Is a Config key a section name (does it start with a capital)?"""
|
||||
if key and key[0].upper()==key[0] and not key.startswith('_'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""An attribute based dict that can do smart merges."""
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
dict.__init__(self, *args, **kwds)
|
||||
self._ensure_subconfig()
|
||||
|
||||
def _ensure_subconfig(self):
|
||||
"""ensure that sub-dicts that should be Config objects are
|
||||
|
||||
casts dicts that are under section keys to Config objects,
|
||||
which is necessary for constructing Config objects from dict literals.
|
||||
"""
|
||||
for key in self:
|
||||
obj = self[key]
|
||||
if _is_section_key(key) \
|
||||
and isinstance(obj, dict) \
|
||||
and not isinstance(obj, Config):
|
||||
setattr(self, key, Config(obj))
|
||||
|
||||
def _merge(self, other):
|
||||
"""deprecated alias, use Config.merge()"""
|
||||
self.merge(other)
|
||||
|
||||
def merge(self, other):
|
||||
"""merge another config object into this one"""
|
||||
to_update = {}
|
||||
for k, v in other.items():
|
||||
if k not in self:
|
||||
to_update[k] = v
|
||||
else: # I have this key
|
||||
if isinstance(v, Config) and isinstance(self[k], 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 collisions(self, other):
|
||||
"""Check for collisions between two config objects.
|
||||
|
||||
Returns a dict of the form {"Class": {"trait": "collision message"}}`,
|
||||
indicating which values have been ignored.
|
||||
|
||||
An empty dict indicates no collisions.
|
||||
"""
|
||||
collisions = {}
|
||||
for section in self:
|
||||
if section not in other:
|
||||
continue
|
||||
mine = self[section]
|
||||
theirs = other[section]
|
||||
for key in mine:
|
||||
if key in theirs and mine[key] != theirs[key]:
|
||||
collisions.setdefault(section, {})
|
||||
collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
|
||||
return collisions
|
||||
|
||||
def __contains__(self, key):
|
||||
# allow nested contains of the form `"Section.key" in config`
|
||||
if '.' in key:
|
||||
first, remainder = key.split('.', 1)
|
||||
if first not in self:
|
||||
return False
|
||||
return remainder in self[first]
|
||||
|
||||
return super(Config, self).__contains__(key)
|
||||
|
||||
# .has_key is deprecated for dictionaries.
|
||||
has_key = __contains__
|
||||
|
||||
def _has_section(self, key):
|
||||
return _is_section_key(key) and key in self
|
||||
|
||||
def copy(self):
|
||||
return type(self)(dict.copy(self))
|
||||
|
||||
def __copy__(self):
|
||||
return self.copy()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
new_config = type(self)()
|
||||
for key, value in self.items():
|
||||
if isinstance(value, (Config, LazyConfigValue)):
|
||||
# deep copy config objects
|
||||
value = copy.deepcopy(value, memo)
|
||||
elif type(value) in {dict, list, set, tuple}:
|
||||
# shallow copy plain container traits
|
||||
value = copy.copy(value)
|
||||
new_config[key] = value
|
||||
return new_config
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
if _is_section_key(key):
|
||||
c = Config()
|
||||
dict.__setitem__(self, key, c)
|
||||
return c
|
||||
elif not key.startswith('_'):
|
||||
# undefined, create lazy value, used for container methods
|
||||
v = LazyConfigValue()
|
||||
dict.__setitem__(self, key, v)
|
||||
return v
|
||||
else:
|
||||
raise KeyError
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if _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))
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key.startswith('__'):
|
||||
return dict.__getattr__(self, key)
|
||||
try:
|
||||
return self.__getitem__(key)
|
||||
except KeyError as e:
|
||||
raise AttributeError(e)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key.startswith('__'):
|
||||
return dict.__setattr__(self, key, value)
|
||||
try:
|
||||
self.__setitem__(key, value)
|
||||
except KeyError as e:
|
||||
raise AttributeError(e)
|
||||
|
||||
def __delattr__(self, key):
|
||||
if key.startswith('__'):
|
||||
return dict.__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:`Config`.
|
||||
|
||||
Notes
|
||||
-----
|
||||
A :class:`ConfigLoader` does one thing: load a config from a source
|
||||
(file, command line arguments) and returns the data as a :class:`Config` object.
|
||||
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 _log_default(self):
|
||||
from traitlets.log import get_logger
|
||||
return get_logger()
|
||||
|
||||
def __init__(self, log=None):
|
||||
"""A base class for config loaders.
|
||||
|
||||
log : instance of :class:`logging.Logger` to use.
|
||||
By default loger of :meth:`traitlets.config.application.Application.instance()`
|
||||
will be used
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> cl = ConfigLoader()
|
||||
>>> config = cl.load_config()
|
||||
>>> config
|
||||
{}
|
||||
"""
|
||||
self.clear()
|
||||
if log is None:
|
||||
self.log = self._log_default()
|
||||
self.log.debug('Using default logger')
|
||||
else:
|
||||
self.log = log
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, path=None, **kw):
|
||||
"""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(FileConfigLoader, self).__init__(**kw)
|
||||
self.filename = filename
|
||||
self.path = path
|
||||
self.full_filename = ''
|
||||
|
||||
def _find_file(self):
|
||||
"""Try to find the file by searching the paths."""
|
||||
self.full_filename = filefind(self.filename, self.path)
|
||||
|
||||
class JSONFileConfigLoader(FileConfigLoader):
|
||||
"""A JSON file loader for config
|
||||
|
||||
Can also act as a context manager that rewrite the configuration file to disk on exit.
|
||||
|
||||
Example::
|
||||
|
||||
with JSONFileConfigLoader('myapp.json','/home/jupyter/configurations/') as c:
|
||||
c.MyNewConfigurable.new_value = 'Updated'
|
||||
|
||||
"""
|
||||
|
||||
def load_config(self):
|
||||
"""Load the config from a file and return it as a Config object."""
|
||||
self.clear()
|
||||
try:
|
||||
self._find_file()
|
||||
except IOError as e:
|
||||
raise ConfigFileNotFound(str(e))
|
||||
dct = self._read_file_as_dict()
|
||||
self.config = self._convert_to_config(dct)
|
||||
return self.config
|
||||
|
||||
def _read_file_as_dict(self):
|
||||
with open(self.full_filename) as f:
|
||||
return json.load(f)
|
||||
|
||||
def _convert_to_config(self, dictionary):
|
||||
if 'version' in dictionary:
|
||||
version = dictionary.pop('version')
|
||||
else:
|
||||
version = 1
|
||||
|
||||
if version == 1:
|
||||
return Config(dictionary)
|
||||
else:
|
||||
raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
|
||||
|
||||
def __enter__(self):
|
||||
self.load_config()
|
||||
return self.config
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""
|
||||
Exit the context manager but do not handle any errors.
|
||||
|
||||
In case of any error, we do not want to write the potentially broken
|
||||
configuration to disk.
|
||||
"""
|
||||
self.config.version = 1
|
||||
json_config = json.dumps(self.config, indent=2)
|
||||
with open(self.full_filename, 'w') as f:
|
||||
f.write(json_config)
|
||||
|
||||
|
||||
|
||||
class PyFileConfigLoader(FileConfigLoader):
|
||||
"""A config loader for pure python files.
|
||||
|
||||
This is responsible for locating a Python config file by filename and
|
||||
path, then executing it to construct a Config object.
|
||||
"""
|
||||
|
||||
def load_config(self):
|
||||
"""Load the config from a file and return it as a Config object."""
|
||||
self.clear()
|
||||
try:
|
||||
self._find_file()
|
||||
except IOError as e:
|
||||
raise ConfigFileNotFound(str(e))
|
||||
self._read_file_as_dict()
|
||||
return self.config
|
||||
|
||||
def load_subconfig(self, fname, path=None):
|
||||
"""Injected into config file namespace as load_subconfig"""
|
||||
if path is None:
|
||||
path = self.path
|
||||
|
||||
loader = self.__class__(fname, path)
|
||||
try:
|
||||
sub_config = loader.load_config()
|
||||
except ConfigFileNotFound:
|
||||
# Pass silently if the sub config is not there,
|
||||
# treat it as an empty config file.
|
||||
pass
|
||||
else:
|
||||
self.config.merge(sub_config)
|
||||
|
||||
def _read_file_as_dict(self):
|
||||
"""Load the config file into self.config, with recursive loading."""
|
||||
def get_config():
|
||||
"""Unnecessary now, but a deprecation warning is more trouble than it's worth."""
|
||||
return self.config
|
||||
|
||||
namespace = dict(
|
||||
c=self.config,
|
||||
load_subconfig=self.load_subconfig,
|
||||
get_config=get_config,
|
||||
__file__=self.full_filename,
|
||||
)
|
||||
fs_encoding = sys.getfilesystemencoding() or 'ascii'
|
||||
conf_filename = self.full_filename.encode(fs_encoding)
|
||||
py3compat.execfile(conf_filename, namespace)
|
||||
|
||||
|
||||
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.<lhs> = <rhs>
|
||||
|
||||
* expands ~ with expanduser
|
||||
* tries to assign with literal_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 = literal_eval(rhs)
|
||||
except (NameError, SyntaxError, ValueError):
|
||||
# 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.items():
|
||||
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, **kw):
|
||||
"""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 traitlets.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})]
|
||||
"""
|
||||
super(KeyValueConfigLoader, self).__init__(**kw)
|
||||
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 stdin.encoding, falling back on default enc"""
|
||||
uargv = []
|
||||
if enc is None:
|
||||
enc = DEFAULT_ENCODING
|
||||
for arg in argv:
|
||||
if not isinstance(arg, text_type):
|
||||
# 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)`.
|
||||
"""
|
||||
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 --matplotlib=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
|
||||
self.log.warning("Unrecognized alias: '%s', it will probably have no effect.", raw)
|
||||
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, log=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__(log=log)
|
||||
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).items():
|
||||
exec("self.config.%s = v"%k, 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.items():
|
||||
if key in flags:
|
||||
# flags
|
||||
nargs = '?'
|
||||
else:
|
||||
nargs = None
|
||||
if len(key) is 1:
|
||||
paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs)
|
||||
else:
|
||||
paa('--'+key, type=text_type, dest=value, nargs=nargs)
|
||||
for key, (value, help) in flags.items():
|
||||
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).items():
|
||||
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(log=self.log)
|
||||
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
|
88
venv/Lib/site-packages/traitlets/config/manager.py
Normal file
88
venv/Lib/site-packages/traitlets/config/manager.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
"""Manager to read and modify config data in JSON files.
|
||||
"""
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import errno
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
||||
from six import PY3
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets.traitlets import Unicode
|
||||
|
||||
|
||||
def recursive_update(target, new):
|
||||
"""Recursively update one dictionary using another.
|
||||
|
||||
None values will delete their keys.
|
||||
"""
|
||||
for k, v in new.items():
|
||||
if isinstance(v, dict):
|
||||
if k not in target:
|
||||
target[k] = {}
|
||||
recursive_update(target[k], v)
|
||||
if not target[k]:
|
||||
# Prune empty subdicts
|
||||
del target[k]
|
||||
|
||||
elif v is None:
|
||||
target.pop(k, None)
|
||||
|
||||
else:
|
||||
target[k] = v
|
||||
|
||||
|
||||
class BaseJSONConfigManager(LoggingConfigurable):
|
||||
"""General JSON config manager
|
||||
|
||||
Deals with persisting/storing config in a json file
|
||||
"""
|
||||
|
||||
config_dir = Unicode('.')
|
||||
|
||||
def ensure_config_dir_exists(self):
|
||||
try:
|
||||
os.makedirs(self.config_dir, 0o755)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def file_name(self, section_name):
|
||||
return os.path.join(self.config_dir, section_name+'.json')
|
||||
|
||||
def get(self, section_name):
|
||||
"""Retrieve the config data for the specified section.
|
||||
|
||||
Returns the data as a dictionary, or an empty dictionary if the file
|
||||
doesn't exist.
|
||||
"""
|
||||
filename = self.file_name(section_name)
|
||||
if os.path.isfile(filename):
|
||||
with io.open(filename, encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def set(self, section_name, data):
|
||||
"""Store the given config data.
|
||||
"""
|
||||
filename = self.file_name(section_name)
|
||||
self.ensure_config_dir_exists()
|
||||
|
||||
if PY3:
|
||||
f = io.open(filename, 'w', encoding='utf-8')
|
||||
else:
|
||||
f = open(filename, 'wb')
|
||||
with f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def update(self, section_name, new_data):
|
||||
"""Modify the config section by recursively updating it with new_data.
|
||||
|
||||
Returns the modified config data as a dictionary.
|
||||
"""
|
||||
data = self.get(section_name)
|
||||
recursive_update(data, new_data)
|
||||
self.set(section_name, data)
|
||||
return data
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,421 @@
|
|||
# coding: utf-8
|
||||
"""
|
||||
Tests for traitlets.config.application.Application
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from io import StringIO
|
||||
from unittest import TestCase
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
pjoin = os.path.join
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from traitlets.config.configurable import Configurable
|
||||
from traitlets.config.loader import Config
|
||||
from traitlets.tests.utils import check_help_output, check_help_all_output
|
||||
|
||||
from traitlets.config.application import (
|
||||
Application
|
||||
)
|
||||
|
||||
from ipython_genutils.tempdir import TemporaryDirectory
|
||||
from traitlets.traitlets import (
|
||||
Bool, Unicode, Integer, List, Dict
|
||||
)
|
||||
|
||||
|
||||
class Foo(Configurable):
|
||||
|
||||
i = Integer(0, help="The integer i.").tag(config=True)
|
||||
j = Integer(1, help="The integer j.").tag(config=True)
|
||||
name = Unicode(u'Brian', help="First name.").tag(config=True)
|
||||
|
||||
|
||||
class Bar(Configurable):
|
||||
|
||||
b = Integer(0, help="The integer b.").tag(config=True)
|
||||
enabled = Bool(True, help="Enable bar.").tag(config=True)
|
||||
|
||||
|
||||
class MyApp(Application):
|
||||
|
||||
name = Unicode(u'myapp')
|
||||
running = Bool(False, help="Is the app running?").tag(config=True)
|
||||
classes = List([Bar, Foo])
|
||||
config_file = Unicode(u'', help="Load this config file").tag(config=True)
|
||||
|
||||
warn_tpyo = Unicode(u"yes the name is wrong on purpose", config=True,
|
||||
help="Should print a warning if `MyApp.warn-typo=...` command is passed")
|
||||
|
||||
aliases = Dict({
|
||||
'i' : 'Foo.i',
|
||||
'j' : 'Foo.j',
|
||||
'name' : 'Foo.name',
|
||||
'enabled' : 'Bar.enabled',
|
||||
'log-level' : 'Application.log_level',
|
||||
})
|
||||
|
||||
flags = Dict(dict(enable=({'Bar': {'enabled' : True}}, "Set Bar.enabled to True"),
|
||||
disable=({'Bar': {'enabled' : False}}, "Set Bar.enabled to False"),
|
||||
crit=({'Application' : {'log_level' : logging.CRITICAL}},
|
||||
"set level=CRITICAL"),
|
||||
))
|
||||
|
||||
def init_foo(self):
|
||||
self.foo = Foo(parent=self)
|
||||
|
||||
def init_bar(self):
|
||||
self.bar = Bar(parent=self)
|
||||
|
||||
|
||||
class TestApplication(TestCase):
|
||||
|
||||
def test_log(self):
|
||||
stream = StringIO()
|
||||
app = MyApp(log_level=logging.INFO)
|
||||
handler = logging.StreamHandler(stream)
|
||||
# trigger reconstruction of the log formatter
|
||||
app.log.handlers = [handler]
|
||||
app.log_format = "%(message)s"
|
||||
app.log_datefmt = "%Y-%m-%d %H:%M"
|
||||
app.log.info("hello")
|
||||
assert "hello" in stream.getvalue()
|
||||
|
||||
def test_basic(self):
|
||||
app = MyApp()
|
||||
self.assertEqual(app.name, u'myapp')
|
||||
self.assertEqual(app.running, False)
|
||||
self.assertEqual(app.classes, [MyApp,Bar,Foo])
|
||||
self.assertEqual(app.config_file, u'')
|
||||
|
||||
def test_config(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
|
||||
config = app.config
|
||||
self.assertEqual(config.Foo.i, 10)
|
||||
self.assertEqual(config.Foo.j, 10)
|
||||
self.assertEqual(config.Bar.enabled, False)
|
||||
self.assertEqual(config.MyApp.log_level,50)
|
||||
|
||||
def test_config_propagation(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])
|
||||
app.init_foo()
|
||||
app.init_bar()
|
||||
self.assertEqual(app.foo.i, 10)
|
||||
self.assertEqual(app.foo.j, 10)
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
|
||||
def test_cli_priority(self):
|
||||
"""Test that loading config files does not override CLI options"""
|
||||
name = 'config.py'
|
||||
class TestApp(Application):
|
||||
value = Unicode().tag(config=True)
|
||||
config_file_loaded = Bool().tag(config=True)
|
||||
aliases = {'v': 'TestApp.value'}
|
||||
app = TestApp()
|
||||
with TemporaryDirectory() as td:
|
||||
config_file = pjoin(td, name)
|
||||
with open(config_file, 'w') as f:
|
||||
f.writelines([
|
||||
"c.TestApp.value = 'config file'\n",
|
||||
"c.TestApp.config_file_loaded = True\n"
|
||||
])
|
||||
|
||||
app.parse_command_line(['--v=cli'])
|
||||
assert 'value' in app.config.TestApp
|
||||
assert app.config.TestApp.value == 'cli'
|
||||
assert app.value == 'cli'
|
||||
|
||||
app.load_config_file(name, path=[td])
|
||||
assert app.config_file_loaded
|
||||
assert app.config.TestApp.value == 'cli'
|
||||
assert app.value == 'cli'
|
||||
|
||||
def test_ipython_cli_priority(self):
|
||||
# this test is almost entirely redundant with above,
|
||||
# but we can keep it around in case of subtle issues creeping into
|
||||
# the exact sequence IPython follows.
|
||||
name = 'config.py'
|
||||
class TestApp(Application):
|
||||
value = Unicode().tag(config=True)
|
||||
config_file_loaded = Bool().tag(config=True)
|
||||
aliases = {'v': 'TestApp.value'}
|
||||
app = TestApp()
|
||||
with TemporaryDirectory() as td:
|
||||
config_file = pjoin(td, name)
|
||||
with open(config_file, 'w') as f:
|
||||
f.writelines([
|
||||
"c.TestApp.value = 'config file'\n",
|
||||
"c.TestApp.config_file_loaded = True\n"
|
||||
])
|
||||
# follow IPython's config-loading sequence to ensure CLI priority is preserved
|
||||
app.parse_command_line(['--v=cli'])
|
||||
# this is where IPython makes a mistake:
|
||||
# it assumes app.config will not be modified,
|
||||
# and storing a reference is storing a copy
|
||||
cli_config = app.config
|
||||
assert 'value' in app.config.TestApp
|
||||
assert app.config.TestApp.value == 'cli'
|
||||
assert app.value == 'cli'
|
||||
app.load_config_file(name, path=[td])
|
||||
assert app.config_file_loaded
|
||||
# enforce cl-opts override config file opts:
|
||||
# this is where IPython makes a mistake: it assumes
|
||||
# that cl_config is a different object, but it isn't.
|
||||
app.update_config(cli_config)
|
||||
assert app.config.TestApp.value == 'cli'
|
||||
assert app.value == 'cli'
|
||||
|
||||
def test_flags(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--disable"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
app.parse_command_line(["--enable"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, True)
|
||||
|
||||
def test_aliases(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--i=5", "--j=10"])
|
||||
app.init_foo()
|
||||
self.assertEqual(app.foo.i, 5)
|
||||
app.init_foo()
|
||||
self.assertEqual(app.foo.j, 10)
|
||||
|
||||
def test_flag_clobber(self):
|
||||
"""test that setting flags doesn't clobber existing settings"""
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--Bar.b=5", "--disable"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
self.assertEqual(app.bar.b, 5)
|
||||
app.parse_command_line(["--enable", "--Bar.b=10"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, True)
|
||||
self.assertEqual(app.bar.b, 10)
|
||||
|
||||
def test_warn_autocorrect(self):
|
||||
stream = StringIO()
|
||||
app = MyApp(log_level=logging.INFO)
|
||||
app.log.handlers = [logging.StreamHandler(stream)]
|
||||
|
||||
cfg = Config()
|
||||
cfg.MyApp.warn_typo = "WOOOO"
|
||||
app.config = cfg
|
||||
|
||||
self.assertIn("warn_typo", stream.getvalue())
|
||||
self.assertIn("warn_tpyo", stream.getvalue())
|
||||
|
||||
|
||||
def test_flatten_flags(self):
|
||||
cfg = Config()
|
||||
cfg.MyApp.log_level = logging.WARN
|
||||
app = MyApp()
|
||||
app.update_config(cfg)
|
||||
self.assertEqual(app.log_level, logging.WARN)
|
||||
self.assertEqual(app.config.MyApp.log_level, logging.WARN)
|
||||
app.initialize(["--crit"])
|
||||
self.assertEqual(app.log_level, logging.CRITICAL)
|
||||
# this would be app.config.Application.log_level if it failed:
|
||||
self.assertEqual(app.config.MyApp.log_level, logging.CRITICAL)
|
||||
|
||||
def test_flatten_aliases(self):
|
||||
cfg = Config()
|
||||
cfg.MyApp.log_level = logging.WARN
|
||||
app = MyApp()
|
||||
app.update_config(cfg)
|
||||
self.assertEqual(app.log_level, logging.WARN)
|
||||
self.assertEqual(app.config.MyApp.log_level, logging.WARN)
|
||||
app.initialize(["--log-level", "CRITICAL"])
|
||||
self.assertEqual(app.log_level, logging.CRITICAL)
|
||||
# this would be app.config.Application.log_level if it failed:
|
||||
self.assertEqual(app.config.MyApp.log_level, "CRITICAL")
|
||||
|
||||
def test_extra_args(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--Bar.b=5", 'extra', "--disable", 'args'])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
self.assertEqual(app.bar.b, 5)
|
||||
self.assertEqual(app.extra_args, ['extra', 'args'])
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--Bar.b=5", '--', 'extra', "--disable", 'args'])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, True)
|
||||
self.assertEqual(app.bar.b, 5)
|
||||
self.assertEqual(app.extra_args, ['extra', '--disable', 'args'])
|
||||
|
||||
def test_unicode_argv(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(['ünîcødé'])
|
||||
|
||||
def test_document_config_option(self):
|
||||
app = MyApp()
|
||||
app.document_config_options()
|
||||
|
||||
def test_generate_config_file(self):
|
||||
app = MyApp()
|
||||
assert 'The integer b.' in app.generate_config_file()
|
||||
|
||||
def test_generate_config_file_classes_to_include(self):
|
||||
class NoTraits(Foo, Bar):
|
||||
pass
|
||||
|
||||
app = MyApp()
|
||||
app.classes.append(NoTraits)
|
||||
conf_txt = app.generate_config_file()
|
||||
self.assertIn('The integer b.', conf_txt)
|
||||
self.assertIn('# Bar(Configurable)', conf_txt)
|
||||
self.assertIn('# Foo(Configurable)', conf_txt)
|
||||
self.assertNotIn('# Configurable', conf_txt)
|
||||
self.assertIn('# NoTraits(Foo,Bar)', conf_txt)
|
||||
|
||||
def test_multi_file(self):
|
||||
app = MyApp()
|
||||
app.log = logging.getLogger()
|
||||
name = 'config.py'
|
||||
with TemporaryDirectory('_1') as td1:
|
||||
with open(pjoin(td1, name), 'w') as f1:
|
||||
f1.write("get_config().MyApp.Bar.b = 1")
|
||||
with TemporaryDirectory('_2') as td2:
|
||||
with open(pjoin(td2, name), 'w') as f2:
|
||||
f2.write("get_config().MyApp.Bar.b = 2")
|
||||
app.load_config_file(name, path=[td2, td1])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.b, 2)
|
||||
app.load_config_file(name, path=[td1, td2])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.b, 1)
|
||||
|
||||
@mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
|
||||
def test_log_collisions(self):
|
||||
app = MyApp()
|
||||
app.log = logging.getLogger()
|
||||
app.log.setLevel(logging.INFO)
|
||||
name = 'config'
|
||||
with TemporaryDirectory('_1') as td:
|
||||
with open(pjoin(td, name + '.py'), 'w') as f:
|
||||
f.write("get_config().Bar.b = 1")
|
||||
with open(pjoin(td, name + '.json'), 'w') as f:
|
||||
json.dump({
|
||||
'Bar': {
|
||||
'b': 2
|
||||
}
|
||||
}, f)
|
||||
with self.assertLogs(app.log, logging.WARNING) as captured:
|
||||
app.load_config_file(name, path=[td])
|
||||
app.init_bar()
|
||||
assert app.bar.b == 2
|
||||
output = '\n'.join(captured.output)
|
||||
assert 'Collision' in output
|
||||
assert '1 ignored, using 2' in output
|
||||
assert pjoin(td, name + '.py') in output
|
||||
assert pjoin(td, name + '.json') in output
|
||||
|
||||
@mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
|
||||
def test_log_bad_config(self):
|
||||
app = MyApp()
|
||||
app.log = logging.getLogger()
|
||||
name = 'config.py'
|
||||
with TemporaryDirectory() as td:
|
||||
with open(pjoin(td, name), 'w') as f:
|
||||
f.write("syntax error()")
|
||||
with self.assertLogs(app.log, logging.ERROR) as captured:
|
||||
app.load_config_file(name, path=[td])
|
||||
output = '\n'.join(captured.output)
|
||||
self.assertIn('SyntaxError', output)
|
||||
|
||||
def test_raise_on_bad_config(self):
|
||||
app = MyApp()
|
||||
app.raise_config_file_errors = True
|
||||
app.log = logging.getLogger()
|
||||
name = 'config.py'
|
||||
with TemporaryDirectory() as td:
|
||||
with open(pjoin(td, name), 'w') as f:
|
||||
f.write("syntax error()")
|
||||
with self.assertRaises(SyntaxError):
|
||||
app.load_config_file(name, path=[td])
|
||||
|
||||
def test_loaded_config_files(self):
|
||||
app = MyApp()
|
||||
app.log = logging.getLogger()
|
||||
name = 'config.py'
|
||||
with TemporaryDirectory('_1') as td1:
|
||||
config_file = pjoin(td1, name)
|
||||
with open(config_file, 'w') as f:
|
||||
f.writelines([
|
||||
"c.MyApp.running = True\n"
|
||||
])
|
||||
|
||||
app.load_config_file(name, path=[td1])
|
||||
self.assertEqual(len(app.loaded_config_files), 1)
|
||||
self.assertEquals(app.loaded_config_files[0], config_file)
|
||||
|
||||
app.start()
|
||||
self.assertEqual(app.running, True)
|
||||
|
||||
# emulate an app that allows dynamic updates and update config file
|
||||
with open(config_file, 'w') as f:
|
||||
f.writelines([
|
||||
"c.MyApp.running = False\n"
|
||||
])
|
||||
|
||||
# reload and verify update, and that loaded_configs was not increased
|
||||
app.load_config_file(name, path=[td1])
|
||||
self.assertEqual(len(app.loaded_config_files), 1)
|
||||
self.assertEqual(app.running, False)
|
||||
|
||||
# Attempt to update, ensure error...
|
||||
with self.assertRaises(AttributeError):
|
||||
app.loaded_config_files = "/foo"
|
||||
|
||||
# ensure it can't be udpated via append
|
||||
app.loaded_config_files.append("/bar")
|
||||
self.assertEqual(len(app.loaded_config_files), 1)
|
||||
|
||||
# repeat to ensure no unexpected changes occurred
|
||||
app.load_config_file(name, path=[td1])
|
||||
self.assertEqual(len(app.loaded_config_files), 1)
|
||||
self.assertEqual(app.running, False)
|
||||
|
||||
|
||||
class DeprecatedApp(Application):
|
||||
override_called = False
|
||||
parent_called = False
|
||||
def _config_changed(self, name, old, new):
|
||||
self.override_called = True
|
||||
def _capture(*args):
|
||||
self.parent_called = True
|
||||
with mock.patch.object(self.log, 'debug', _capture):
|
||||
super(DeprecatedApp, self)._config_changed(name, old, new)
|
||||
|
||||
|
||||
def test_deprecated_notifier():
|
||||
app = DeprecatedApp()
|
||||
assert not app.override_called
|
||||
assert not app.parent_called
|
||||
app.config = Config({'A': {'b': 'c'}})
|
||||
assert app.override_called
|
||||
assert app.parent_called
|
||||
|
||||
|
||||
def test_help_output():
|
||||
check_help_output(__name__)
|
||||
check_help_all_output(__name__)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# for test_help_output:
|
||||
MyApp.launch_instance()
|
|
@ -0,0 +1,459 @@
|
|||
# encoding: utf-8
|
||||
"""Tests for traitlets.config.configurable"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import logging
|
||||
from unittest import TestCase
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from traitlets.config.configurable import (
|
||||
Configurable,
|
||||
LoggingConfigurable,
|
||||
SingletonConfigurable,
|
||||
)
|
||||
|
||||
from traitlets.traitlets import (
|
||||
Integer, Float, Unicode, List, Dict, Set,
|
||||
_deprecations_shown,
|
||||
)
|
||||
|
||||
from traitlets.config.loader import Config
|
||||
from six import PY3
|
||||
|
||||
from ...tests._warnings import expected_warnings
|
||||
|
||||
class MyConfigurable(Configurable):
|
||||
a = Integer(1, help="The integer a.").tag(config=True)
|
||||
b = Float(1.0, help="The integer b.").tag(config=True)
|
||||
c = Unicode('no config')
|
||||
|
||||
|
||||
mc_help=u"""MyConfigurable options
|
||||
----------------------
|
||||
--MyConfigurable.a=<Integer>
|
||||
Default: 1
|
||||
The integer a.
|
||||
--MyConfigurable.b=<Float>
|
||||
Default: 1.0
|
||||
The integer b."""
|
||||
|
||||
mc_help_inst=u"""MyConfigurable options
|
||||
----------------------
|
||||
--MyConfigurable.a=<Integer>
|
||||
Current: 5
|
||||
The integer a.
|
||||
--MyConfigurable.b=<Float>
|
||||
Current: 4.0
|
||||
The integer b."""
|
||||
|
||||
# On Python 3, the Integer trait is a synonym for Int
|
||||
if PY3:
|
||||
mc_help = mc_help.replace(u"<Integer>", u"<Int>")
|
||||
mc_help_inst = mc_help_inst.replace(u"<Integer>", u"<Int>")
|
||||
|
||||
class Foo(Configurable):
|
||||
a = Integer(0, help="The integer a.").tag(config=True)
|
||||
b = Unicode('nope').tag(config=True)
|
||||
|
||||
|
||||
class Bar(Foo):
|
||||
b = Unicode('gotit', help="The string b.").tag(config=False)
|
||||
c = Float(help="The string c.").tag(config=True)
|
||||
|
||||
|
||||
class TestConfigurable(TestCase):
|
||||
|
||||
def test_default(self):
|
||||
c1 = Configurable()
|
||||
c2 = Configurable(config=c1.config)
|
||||
c3 = Configurable(config=c2.config)
|
||||
self.assertEqual(c1.config, c2.config)
|
||||
self.assertEqual(c2.config, c3.config)
|
||||
|
||||
def test_custom(self):
|
||||
config = Config()
|
||||
config.foo = 'foo'
|
||||
config.bar = 'bar'
|
||||
c1 = Configurable(config=config)
|
||||
c2 = Configurable(config=c1.config)
|
||||
c3 = Configurable(config=c2.config)
|
||||
self.assertEqual(c1.config, config)
|
||||
self.assertEqual(c2.config, config)
|
||||
self.assertEqual(c3.config, config)
|
||||
# Test that copies are not made
|
||||
self.assertTrue(c1.config is config)
|
||||
self.assertTrue(c2.config is config)
|
||||
self.assertTrue(c3.config is config)
|
||||
self.assertTrue(c1.config is c2.config)
|
||||
self.assertTrue(c2.config is c3.config)
|
||||
|
||||
def test_inheritance(self):
|
||||
config = Config()
|
||||
config.MyConfigurable.a = 2
|
||||
config.MyConfigurable.b = 2.0
|
||||
c1 = MyConfigurable(config=config)
|
||||
c2 = MyConfigurable(config=c1.config)
|
||||
self.assertEqual(c1.a, config.MyConfigurable.a)
|
||||
self.assertEqual(c1.b, config.MyConfigurable.b)
|
||||
self.assertEqual(c2.a, config.MyConfigurable.a)
|
||||
self.assertEqual(c2.b, config.MyConfigurable.b)
|
||||
|
||||
def test_parent(self):
|
||||
config = Config()
|
||||
config.Foo.a = 10
|
||||
config.Foo.b = "wow"
|
||||
config.Bar.b = 'later'
|
||||
config.Bar.c = 100.0
|
||||
f = Foo(config=config)
|
||||
with expected_warnings(['`b` not recognized']):
|
||||
b = Bar(config=f.config)
|
||||
self.assertEqual(f.a, 10)
|
||||
self.assertEqual(f.b, 'wow')
|
||||
self.assertEqual(b.b, 'gotit')
|
||||
self.assertEqual(b.c, 100.0)
|
||||
|
||||
def test_override1(self):
|
||||
config = Config()
|
||||
config.MyConfigurable.a = 2
|
||||
config.MyConfigurable.b = 2.0
|
||||
c = MyConfigurable(a=3, config=config)
|
||||
self.assertEqual(c.a, 3)
|
||||
self.assertEqual(c.b, config.MyConfigurable.b)
|
||||
self.assertEqual(c.c, 'no config')
|
||||
|
||||
def test_override2(self):
|
||||
config = Config()
|
||||
config.Foo.a = 1
|
||||
config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
|
||||
config.Bar.c = 10.0
|
||||
with expected_warnings(['`b` not recognized']):
|
||||
c = Bar(config=config)
|
||||
self.assertEqual(c.a, config.Foo.a)
|
||||
self.assertEqual(c.b, 'gotit')
|
||||
self.assertEqual(c.c, config.Bar.c)
|
||||
with expected_warnings(['`b` not recognized']):
|
||||
c = Bar(a=2, b='and', c=20.0, config=config)
|
||||
self.assertEqual(c.a, 2)
|
||||
self.assertEqual(c.b, 'and')
|
||||
self.assertEqual(c.c, 20.0)
|
||||
|
||||
def test_help(self):
|
||||
self.assertEqual(MyConfigurable.class_get_help(), mc_help)
|
||||
|
||||
def test_help_inst(self):
|
||||
inst = MyConfigurable(a=5, b=4)
|
||||
self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
|
||||
|
||||
|
||||
class TestSingletonConfigurable(TestCase):
|
||||
|
||||
def test_instance(self):
|
||||
class Foo(SingletonConfigurable): pass
|
||||
self.assertEqual(Foo.initialized(), False)
|
||||
foo = Foo.instance()
|
||||
self.assertEqual(Foo.initialized(), True)
|
||||
self.assertEqual(foo, Foo.instance())
|
||||
self.assertEqual(SingletonConfigurable._instance, None)
|
||||
|
||||
def test_inheritance(self):
|
||||
class Bar(SingletonConfigurable): pass
|
||||
class Bam(Bar): pass
|
||||
self.assertEqual(Bar.initialized(), False)
|
||||
self.assertEqual(Bam.initialized(), False)
|
||||
bam = Bam.instance()
|
||||
bam == Bar.instance()
|
||||
self.assertEqual(Bar.initialized(), True)
|
||||
self.assertEqual(Bam.initialized(), True)
|
||||
self.assertEqual(bam, Bam._instance)
|
||||
self.assertEqual(bam, Bar._instance)
|
||||
self.assertEqual(SingletonConfigurable._instance, None)
|
||||
|
||||
|
||||
class MyParent(Configurable):
|
||||
pass
|
||||
|
||||
class MyParent2(MyParent):
|
||||
pass
|
||||
|
||||
class TestParentConfigurable(TestCase):
|
||||
|
||||
def test_parent_config(self):
|
||||
cfg = Config({
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 2.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
parent = MyParent(config=cfg)
|
||||
myc = MyConfigurable(parent=parent)
|
||||
self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
|
||||
|
||||
def test_parent_inheritance(self):
|
||||
cfg = Config({
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 2.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
parent = MyParent2(config=cfg)
|
||||
myc = MyConfigurable(parent=parent)
|
||||
self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
|
||||
|
||||
def test_multi_parent(self):
|
||||
cfg = Config({
|
||||
'MyParent2' : {
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 2.0,
|
||||
}
|
||||
},
|
||||
# this one shouldn't count
|
||||
'MyConfigurable' : {
|
||||
'b' : 3.0,
|
||||
},
|
||||
}
|
||||
})
|
||||
parent2 = MyParent2(config=cfg)
|
||||
parent = MyParent(parent=parent2)
|
||||
myc = MyConfigurable(parent=parent)
|
||||
self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
|
||||
|
||||
def test_parent_priority(self):
|
||||
cfg = Config({
|
||||
'MyConfigurable' : {
|
||||
'b' : 2.0,
|
||||
},
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 3.0,
|
||||
}
|
||||
},
|
||||
'MyParent2' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 4.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
parent = MyParent2(config=cfg)
|
||||
myc = MyConfigurable(parent=parent)
|
||||
self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b)
|
||||
|
||||
def test_multi_parent_priority(self):
|
||||
cfg = Config({
|
||||
'MyConfigurable' : {
|
||||
'b' : 2.0,
|
||||
},
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 3.0,
|
||||
}
|
||||
},
|
||||
'MyParent2' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 4.0,
|
||||
}
|
||||
},
|
||||
'MyParent2' : {
|
||||
'MyParent' : {
|
||||
'MyConfigurable' : {
|
||||
'b' : 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
parent2 = MyParent2(config=cfg)
|
||||
parent = MyParent2(parent=parent2)
|
||||
myc = MyConfigurable(parent=parent)
|
||||
self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
|
||||
|
||||
class Containers(Configurable):
|
||||
lis = List().tag(config=True)
|
||||
def _lis_default(self):
|
||||
return [-1]
|
||||
|
||||
s = Set().tag(config=True)
|
||||
def _s_default(self):
|
||||
return {'a'}
|
||||
|
||||
d = Dict().tag(config=True)
|
||||
def _d_default(self):
|
||||
return {'a' : 'b'}
|
||||
|
||||
class TestConfigContainers(TestCase):
|
||||
def test_extend(self):
|
||||
c = Config()
|
||||
c.Containers.lis.extend(list(range(5)))
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, list(range(-1,5)))
|
||||
|
||||
def test_insert(self):
|
||||
c = Config()
|
||||
c.Containers.lis.insert(0, 'a')
|
||||
c.Containers.lis.insert(1, 'b')
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, ['a', 'b', -1])
|
||||
|
||||
def test_prepend(self):
|
||||
c = Config()
|
||||
c.Containers.lis.prepend([1,2])
|
||||
c.Containers.lis.prepend([2,3])
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, [2,3,1,2,-1])
|
||||
|
||||
def test_prepend_extend(self):
|
||||
c = Config()
|
||||
c.Containers.lis.prepend([1,2])
|
||||
c.Containers.lis.extend([2,3])
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, [1,2,-1,2,3])
|
||||
|
||||
def test_append_extend(self):
|
||||
c = Config()
|
||||
c.Containers.lis.append([1,2])
|
||||
c.Containers.lis.extend([2,3])
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, [-1,[1,2],2,3])
|
||||
|
||||
def test_extend_append(self):
|
||||
c = Config()
|
||||
c.Containers.lis.extend([2,3])
|
||||
c.Containers.lis.append([1,2])
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, [-1,2,3,[1,2]])
|
||||
|
||||
def test_insert_extend(self):
|
||||
c = Config()
|
||||
c.Containers.lis.insert(0, 1)
|
||||
c.Containers.lis.extend([2,3])
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.lis, [1,-1,2,3])
|
||||
|
||||
def test_set_update(self):
|
||||
c = Config()
|
||||
c.Containers.s.update({0,1,2})
|
||||
c.Containers.s.update({3})
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.s, {'a', 0, 1, 2, 3})
|
||||
|
||||
def test_dict_update(self):
|
||||
c = Config()
|
||||
c.Containers.d.update({'c' : 'd'})
|
||||
c.Containers.d.update({'e' : 'f'})
|
||||
obj = Containers(config=c)
|
||||
self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
|
||||
|
||||
def test_update_twice(self):
|
||||
c = Config()
|
||||
c.MyConfigurable.a = 5
|
||||
m = MyConfigurable(config=c)
|
||||
self.assertEqual(m.a, 5)
|
||||
|
||||
c2 = Config()
|
||||
c2.MyConfigurable.a = 10
|
||||
m.update_config(c2)
|
||||
self.assertEqual(m.a, 10)
|
||||
|
||||
c2.MyConfigurable.a = 15
|
||||
m.update_config(c2)
|
||||
self.assertEqual(m.a, 15)
|
||||
|
||||
def test_update_self(self):
|
||||
"""update_config with same config object still triggers config_changed"""
|
||||
c = Config()
|
||||
c.MyConfigurable.a = 5
|
||||
m = MyConfigurable(config=c)
|
||||
self.assertEqual(m.a, 5)
|
||||
c.MyConfigurable.a = 10
|
||||
m.update_config(c)
|
||||
self.assertEqual(m.a, 10)
|
||||
|
||||
def test_config_default(self):
|
||||
class SomeSingleton(SingletonConfigurable):
|
||||
pass
|
||||
|
||||
class DefaultConfigurable(Configurable):
|
||||
a = Integer().tag(config=True)
|
||||
def _config_default(self):
|
||||
if SomeSingleton.initialized():
|
||||
return SomeSingleton.instance().config
|
||||
return Config()
|
||||
|
||||
c = Config()
|
||||
c.DefaultConfigurable.a = 5
|
||||
|
||||
d1 = DefaultConfigurable()
|
||||
self.assertEqual(d1.a, 0)
|
||||
|
||||
single = SomeSingleton.instance(config=c)
|
||||
|
||||
d2 = DefaultConfigurable()
|
||||
self.assertIs(d2.config, single.config)
|
||||
self.assertEqual(d2.a, 5)
|
||||
|
||||
def test_config_default_deprecated(self):
|
||||
"""Make sure configurables work even with the deprecations in traitlets"""
|
||||
class SomeSingleton(SingletonConfigurable):
|
||||
pass
|
||||
|
||||
# reset deprecation limiter
|
||||
_deprecations_shown.clear()
|
||||
with expected_warnings([]):
|
||||
class DefaultConfigurable(Configurable):
|
||||
a = Integer(config=True)
|
||||
def _config_default(self):
|
||||
if SomeSingleton.initialized():
|
||||
return SomeSingleton.instance().config
|
||||
return Config()
|
||||
|
||||
c = Config()
|
||||
c.DefaultConfigurable.a = 5
|
||||
|
||||
d1 = DefaultConfigurable()
|
||||
self.assertEqual(d1.a, 0)
|
||||
|
||||
single = SomeSingleton.instance(config=c)
|
||||
|
||||
d2 = DefaultConfigurable()
|
||||
self.assertIs(d2.config, single.config)
|
||||
self.assertEqual(d2.a, 5)
|
||||
|
||||
|
||||
class TestLogger(TestCase):
|
||||
|
||||
class A(LoggingConfigurable):
|
||||
foo = Integer(config=True)
|
||||
bar = Integer(config=True)
|
||||
baz = Integer(config=True)
|
||||
|
||||
@mark.skipif(not hasattr(TestCase, 'assertLogs'), reason='requires TestCase.assertLogs')
|
||||
def test_warn_match(self):
|
||||
logger = logging.getLogger('test_warn_match')
|
||||
cfg = Config({'A': {'bat': 5}})
|
||||
with self.assertLogs(logger, logging.WARNING) as captured:
|
||||
a = TestLogger.A(config=cfg, log=logger)
|
||||
|
||||
output = '\n'.join(captured.output)
|
||||
self.assertIn('Did you mean one of: `bar, baz`?', output)
|
||||
self.assertIn('Config option `bat` not recognized by `A`.', output)
|
||||
|
||||
cfg = Config({'A': {'fool': 5}})
|
||||
with self.assertLogs(logger, logging.WARNING) as captured:
|
||||
a = TestLogger.A(config=cfg, log=logger)
|
||||
|
||||
output = '\n'.join(captured.output)
|
||||
self.assertIn('Config option `fool` not recognized by `A`.', output)
|
||||
self.assertIn('Did you mean `foo`?', output)
|
||||
|
||||
cfg = Config({'A': {'totally_wrong': 5}})
|
||||
with self.assertLogs(logger, logging.WARNING) as captured:
|
||||
a = TestLogger.A(config=cfg, log=logger)
|
||||
|
||||
output = '\n'.join(captured.output)
|
||||
self.assertIn('Config option `totally_wrong` not recognized by `A`.', output)
|
||||
self.assertNotIn('Did you mean', output)
|
||||
|
453
venv/Lib/site-packages/traitlets/config/tests/test_loader.py
Normal file
453
venv/Lib/site-packages/traitlets/config/tests/test_loader.py
Normal file
|
@ -0,0 +1,453 @@
|
|||
# encoding: utf-8
|
||||
"""Tests for traitlets.config.loader"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from tempfile import mkstemp
|
||||
from unittest import TestCase
|
||||
|
||||
from pytest import skip
|
||||
|
||||
from traitlets.config.loader import (
|
||||
Config,
|
||||
LazyConfigValue,
|
||||
PyFileConfigLoader,
|
||||
JSONFileConfigLoader,
|
||||
KeyValueConfigLoader,
|
||||
ArgParseConfigLoader,
|
||||
KVArgParseConfigLoader,
|
||||
ConfigError,
|
||||
)
|
||||
|
||||
|
||||
pyfile = """
|
||||
c = get_config()
|
||||
c.a=10
|
||||
c.b=20
|
||||
c.Foo.Bar.value=10
|
||||
c.Foo.Bam.value=list(range(10))
|
||||
c.D.C.value='hi there'
|
||||
"""
|
||||
|
||||
json1file = """
|
||||
{
|
||||
"version": 1,
|
||||
"a": 10,
|
||||
"b": 20,
|
||||
"Foo": {
|
||||
"Bam": {
|
||||
"value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
},
|
||||
"Bar": {
|
||||
"value": 10
|
||||
}
|
||||
},
|
||||
"D": {
|
||||
"C": {
|
||||
"value": "hi there"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# should not load
|
||||
json2file = """
|
||||
{
|
||||
"version": 2
|
||||
}
|
||||
"""
|
||||
|
||||
import logging
|
||||
log = logging.getLogger('devnull')
|
||||
log.setLevel(0)
|
||||
|
||||
class TestFileCL(TestCase):
|
||||
|
||||
def _check_conf(self, config):
|
||||
self.assertEqual(config.a, 10)
|
||||
self.assertEqual(config.b, 20)
|
||||
self.assertEqual(config.Foo.Bar.value, 10)
|
||||
self.assertEqual(config.Foo.Bam.value, list(range(10)))
|
||||
self.assertEqual(config.D.C.value, 'hi there')
|
||||
|
||||
def test_python(self):
|
||||
fd, fname = mkstemp('.py')
|
||||
f = os.fdopen(fd, 'w')
|
||||
f.write(pyfile)
|
||||
f.close()
|
||||
# Unlink the file
|
||||
cl = PyFileConfigLoader(fname, log=log)
|
||||
config = cl.load_config()
|
||||
self._check_conf(config)
|
||||
|
||||
def test_json(self):
|
||||
fd, fname = mkstemp('.json')
|
||||
f = os.fdopen(fd, 'w')
|
||||
f.write(json1file)
|
||||
f.close()
|
||||
# Unlink the file
|
||||
cl = JSONFileConfigLoader(fname, log=log)
|
||||
config = cl.load_config()
|
||||
self._check_conf(config)
|
||||
|
||||
def test_context_manager(self):
|
||||
|
||||
fd, fname = mkstemp('.json')
|
||||
f = os.fdopen(fd, 'w')
|
||||
f.write('{}')
|
||||
f.close()
|
||||
|
||||
cl = JSONFileConfigLoader(fname, log=log)
|
||||
|
||||
value = 'context_manager'
|
||||
|
||||
with cl as c:
|
||||
c.MyAttr.value = value
|
||||
|
||||
self.assertEqual(cl.config.MyAttr.value, value)
|
||||
|
||||
# check that another loader does see the change
|
||||
cl2 = JSONFileConfigLoader(fname, log=log)
|
||||
self.assertEqual(cl.config.MyAttr.value, value)
|
||||
|
||||
def test_json_context_bad_write(self):
|
||||
fd, fname = mkstemp('.json')
|
||||
f = os.fdopen(fd, 'w')
|
||||
f.write('{}')
|
||||
f.close()
|
||||
|
||||
with JSONFileConfigLoader(fname, log=log) as config:
|
||||
config.A.b = 1
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
with JSONFileConfigLoader(fname, log=log) as config:
|
||||
config.A.cant_json = lambda x: x
|
||||
|
||||
loader = JSONFileConfigLoader(fname, log=log)
|
||||
cfg = loader.load_config()
|
||||
assert cfg.A.b == 1
|
||||
assert 'cant_json' not in cfg.A
|
||||
|
||||
def test_collision(self):
|
||||
a = Config()
|
||||
b = Config()
|
||||
self.assertEqual(a.collisions(b), {})
|
||||
a.A.trait1 = 1
|
||||
b.A.trait2 = 2
|
||||
self.assertEqual(a.collisions(b), {})
|
||||
b.A.trait1 = 1
|
||||
self.assertEqual(a.collisions(b), {})
|
||||
b.A.trait1 = 0
|
||||
self.assertEqual(a.collisions(b), {
|
||||
'A': {
|
||||
'trait1': "1 ignored, using 0",
|
||||
}
|
||||
})
|
||||
self.assertEqual(b.collisions(a), {
|
||||
'A': {
|
||||
'trait1': "0 ignored, using 1",
|
||||
}
|
||||
})
|
||||
a.A.trait2 = 3
|
||||
self.assertEqual(b.collisions(a), {
|
||||
'A': {
|
||||
'trait1': "0 ignored, using 1",
|
||||
'trait2': "2 ignored, using 3",
|
||||
}
|
||||
})
|
||||
|
||||
def test_v2raise(self):
|
||||
fd, fname = mkstemp('.json')
|
||||
f = os.fdopen(fd, 'w')
|
||||
f.write(json2file)
|
||||
f.close()
|
||||
# Unlink the file
|
||||
cl = JSONFileConfigLoader(fname, log=log)
|
||||
with self.assertRaises(ValueError):
|
||||
cl.load_config()
|
||||
|
||||
|
||||
class MyLoader1(ArgParseConfigLoader):
|
||||
def _add_arguments(self, aliases=None, flags=None):
|
||||
p = self.parser
|
||||
p.add_argument('-f', '--foo', dest='Global.foo', type=str)
|
||||
p.add_argument('-b', dest='MyClass.bar', type=int)
|
||||
p.add_argument('-n', dest='n', action='store_true')
|
||||
p.add_argument('Global.bam', type=str)
|
||||
|
||||
class MyLoader2(ArgParseConfigLoader):
|
||||
def _add_arguments(self, aliases=None, flags=None):
|
||||
subparsers = self.parser.add_subparsers(dest='subparser_name')
|
||||
subparser1 = subparsers.add_parser('1')
|
||||
subparser1.add_argument('-x',dest='Global.x')
|
||||
subparser2 = subparsers.add_parser('2')
|
||||
subparser2.add_argument('y')
|
||||
|
||||
class TestArgParseCL(TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
cl = MyLoader1()
|
||||
config = cl.load_config('-f hi -b 10 -n wow'.split())
|
||||
self.assertEqual(config.Global.foo, 'hi')
|
||||
self.assertEqual(config.MyClass.bar, 10)
|
||||
self.assertEqual(config.n, True)
|
||||
self.assertEqual(config.Global.bam, 'wow')
|
||||
config = cl.load_config(['wow'])
|
||||
self.assertEqual(list(config.keys()), ['Global'])
|
||||
self.assertEqual(list(config.Global.keys()), ['bam'])
|
||||
self.assertEqual(config.Global.bam, 'wow')
|
||||
|
||||
def test_add_arguments(self):
|
||||
cl = MyLoader2()
|
||||
config = cl.load_config('2 frobble'.split())
|
||||
self.assertEqual(config.subparser_name, '2')
|
||||
self.assertEqual(config.y, 'frobble')
|
||||
config = cl.load_config('1 -x frobble'.split())
|
||||
self.assertEqual(config.subparser_name, '1')
|
||||
self.assertEqual(config.Global.x, 'frobble')
|
||||
|
||||
def test_argv(self):
|
||||
cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
|
||||
config = cl.load_config()
|
||||
self.assertEqual(config.Global.foo, 'hi')
|
||||
self.assertEqual(config.MyClass.bar, 10)
|
||||
self.assertEqual(config.n, True)
|
||||
self.assertEqual(config.Global.bam, 'wow')
|
||||
|
||||
|
||||
class TestKeyValueCL(TestCase):
|
||||
klass = KeyValueConfigLoader
|
||||
|
||||
def test_eval(self):
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config('--Class.str_trait=all --Class.int_trait=5 --Class.list_trait=["hello",5]'.split())
|
||||
self.assertEqual(config.Class.str_trait, 'all')
|
||||
self.assertEqual(config.Class.int_trait, 5)
|
||||
self.assertEqual(config.Class.list_trait, ["hello", 5])
|
||||
|
||||
def test_basic(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = [ '--' + s[2:] for s in pyfile.split('\n') if s.startswith('c.') ]
|
||||
print(argv)
|
||||
config = cl.load_config(argv)
|
||||
self.assertEqual(config.a, 10)
|
||||
self.assertEqual(config.b, 20)
|
||||
self.assertEqual(config.Foo.Bar.value, 10)
|
||||
# non-literal expressions are not evaluated
|
||||
self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
|
||||
self.assertEqual(config.D.C.value, 'hi there')
|
||||
|
||||
def test_expanduser(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
|
||||
config = cl.load_config(argv)
|
||||
self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
|
||||
self.assertEqual(config.b, os.path.expanduser('~'))
|
||||
self.assertEqual(config.c, os.path.expanduser('~/'))
|
||||
self.assertEqual(config.d, '~/')
|
||||
|
||||
def test_extra_args(self):
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
|
||||
self.assertEqual(cl.extra_args, ['b', 'd'])
|
||||
self.assertEqual(config.a, 5)
|
||||
self.assertEqual(config.c, 10)
|
||||
config = cl.load_config(['--', '--a=5', '--c=10'])
|
||||
self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
|
||||
|
||||
def test_unicode_args(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = [u'--a=épsîlön']
|
||||
config = cl.load_config(argv)
|
||||
self.assertEqual(config.a, u'épsîlön')
|
||||
|
||||
def test_unicode_bytes_args(self):
|
||||
uarg = u'--a=é'
|
||||
try:
|
||||
barg = uarg.encode(sys.stdin.encoding)
|
||||
except (TypeError, UnicodeEncodeError):
|
||||
raise skip("sys.stdin.encoding can't handle 'é'")
|
||||
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config([barg])
|
||||
self.assertEqual(config.a, u'é')
|
||||
|
||||
def test_unicode_alias(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = [u'--a=épsîlön']
|
||||
config = cl.load_config(argv, aliases=dict(a='A.a'))
|
||||
self.assertEqual(config.A.a, u'épsîlön')
|
||||
|
||||
|
||||
class TestArgParseKVCL(TestKeyValueCL):
|
||||
klass = KVArgParseConfigLoader
|
||||
|
||||
def test_expanduser2(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
|
||||
config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
|
||||
self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
|
||||
self.assertEqual(config.A.b, '~/1/2/3')
|
||||
|
||||
def test_eval(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ['-c', 'a=5']
|
||||
config = cl.load_config(argv, aliases=dict(c='A.c'))
|
||||
self.assertEqual(config.A.c, u"a=5")
|
||||
|
||||
|
||||
class TestConfig(TestCase):
|
||||
|
||||
def test_setget(self):
|
||||
c = Config()
|
||||
c.a = 10
|
||||
self.assertEqual(c.a, 10)
|
||||
self.assertEqual('b' in c, False)
|
||||
|
||||
def test_auto_section(self):
|
||||
c = Config()
|
||||
self.assertNotIn('A', c)
|
||||
assert not c._has_section('A')
|
||||
A = c.A
|
||||
A.foo = 'hi there'
|
||||
self.assertIn('A', c)
|
||||
assert c._has_section('A')
|
||||
self.assertEqual(c.A.foo, 'hi there')
|
||||
del c.A
|
||||
self.assertEqual(c.A, Config())
|
||||
|
||||
def test_merge_doesnt_exist(self):
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
c2.bar = 10
|
||||
c2.Foo.bar = 10
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Foo.bar, 10)
|
||||
self.assertEqual(c1.bar, 10)
|
||||
c2.Bar.bar = 10
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Bar.bar, 10)
|
||||
|
||||
def test_merge_exists(self):
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
c1.Foo.bar = 10
|
||||
c1.Foo.bam = 30
|
||||
c2.Foo.bar = 20
|
||||
c2.Foo.wow = 40
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Foo.bam, 30)
|
||||
self.assertEqual(c1.Foo.bar, 20)
|
||||
self.assertEqual(c1.Foo.wow, 40)
|
||||
c2.Foo.Bam.bam = 10
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Foo.Bam.bam, 10)
|
||||
|
||||
def test_deepcopy(self):
|
||||
c1 = Config()
|
||||
c1.Foo.bar = 10
|
||||
c1.Foo.bam = 30
|
||||
c1.a = 'asdf'
|
||||
c1.b = range(10)
|
||||
c1.Test.logger = logging.Logger('test')
|
||||
c1.Test.get_logger = logging.getLogger('test')
|
||||
c2 = copy.deepcopy(c1)
|
||||
self.assertEqual(c1, c2)
|
||||
self.assertTrue(c1 is not c2)
|
||||
self.assertTrue(c1.Foo is not c2.Foo)
|
||||
self.assertTrue(c1.Test is not c2.Test)
|
||||
self.assertTrue(c1.Test.logger is c2.Test.logger)
|
||||
self.assertTrue(c1.Test.get_logger is c2.Test.get_logger)
|
||||
|
||||
def test_builtin(self):
|
||||
c1 = Config()
|
||||
c1.format = "json"
|
||||
|
||||
def test_fromdict(self):
|
||||
c1 = Config({'Foo' : {'bar' : 1}})
|
||||
self.assertEqual(c1.Foo.__class__, Config)
|
||||
self.assertEqual(c1.Foo.bar, 1)
|
||||
|
||||
def test_fromdictmerge(self):
|
||||
c1 = Config()
|
||||
c2 = Config({'Foo' : {'bar' : 1}})
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Foo.__class__, Config)
|
||||
self.assertEqual(c1.Foo.bar, 1)
|
||||
|
||||
def test_fromdictmerge2(self):
|
||||
c1 = Config({'Foo' : {'baz' : 2}})
|
||||
c2 = Config({'Foo' : {'bar' : 1}})
|
||||
c1.merge(c2)
|
||||
self.assertEqual(c1.Foo.__class__, Config)
|
||||
self.assertEqual(c1.Foo.bar, 1)
|
||||
self.assertEqual(c1.Foo.baz, 2)
|
||||
self.assertNotIn('baz', c2.Foo)
|
||||
|
||||
def test_contains(self):
|
||||
c1 = Config({'Foo' : {'baz' : 2}})
|
||||
c2 = Config({'Foo' : {'bar' : 1}})
|
||||
self.assertIn('Foo', c1)
|
||||
self.assertIn('Foo.baz', c1)
|
||||
self.assertIn('Foo.bar', c2)
|
||||
self.assertNotIn('Foo.bar', c1)
|
||||
|
||||
def test_pickle_config(self):
|
||||
cfg = Config()
|
||||
cfg.Foo.bar = 1
|
||||
pcfg = pickle.dumps(cfg)
|
||||
cfg2 = pickle.loads(pcfg)
|
||||
self.assertEqual(cfg2, cfg)
|
||||
|
||||
def test_getattr_section(self):
|
||||
cfg = Config()
|
||||
self.assertNotIn('Foo', cfg)
|
||||
Foo = cfg.Foo
|
||||
assert isinstance(Foo, Config)
|
||||
self.assertIn('Foo', cfg)
|
||||
|
||||
def test_getitem_section(self):
|
||||
cfg = Config()
|
||||
self.assertNotIn('Foo', cfg)
|
||||
Foo = cfg['Foo']
|
||||
assert isinstance(Foo, Config)
|
||||
self.assertIn('Foo', cfg)
|
||||
|
||||
def test_getattr_not_section(self):
|
||||
cfg = Config()
|
||||
self.assertNotIn('foo', cfg)
|
||||
foo = cfg.foo
|
||||
assert isinstance(foo, LazyConfigValue)
|
||||
self.assertIn('foo', cfg)
|
||||
|
||||
def test_getattr_private_missing(self):
|
||||
cfg = Config()
|
||||
self.assertNotIn('_repr_html_', cfg)
|
||||
with self.assertRaises(AttributeError):
|
||||
_ = cfg._repr_html_
|
||||
self.assertNotIn('_repr_html_', cfg)
|
||||
self.assertEqual(len(cfg), 0)
|
||||
|
||||
def test_getitem_not_section(self):
|
||||
cfg = Config()
|
||||
self.assertNotIn('foo', cfg)
|
||||
foo = cfg['foo']
|
||||
assert isinstance(foo, LazyConfigValue)
|
||||
self.assertIn('foo', cfg)
|
||||
|
||||
def test_merge_no_copies(self):
|
||||
c = Config()
|
||||
c2 = Config()
|
||||
c2.Foo.trait = []
|
||||
c.merge(c2)
|
||||
c2.Foo.trait.append(1)
|
||||
self.assertIs(c.Foo, c2.Foo)
|
||||
self.assertEqual(c.Foo.trait, [1])
|
||||
self.assertEqual(c2.Foo.trait, [1])
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue