init
This commit is contained in:
commit
38355d2442
9083 changed files with 1225834 additions and 0 deletions
|
|
@ -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.
Binary file not shown.
|
|
@ -0,0 +1,898 @@
|
|||
"""A base class for a configurable application."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
from copy import deepcopy
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
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 ..utils.importstring import import_item
|
||||
from ..utils import cast_unicode
|
||||
from traitlets.utils.text import indent, wrap_paragraphs
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Descriptions for the various sections
|
||||
#-----------------------------------------------------------------------------
|
||||
# merge flags&aliases into options
|
||||
option_description = """
|
||||
The options below are convenience aliases to configurable class-options,
|
||||
as listed in the "Equivalent to" description-line of the aliases.
|
||||
To see all configurable class-options for some <cmd>, use:
|
||||
<cmd> --help-all
|
||||
""".strip() # trim newlines of front and back
|
||||
|
||||
keyvalue_description = """
|
||||
The command-line option below sets the respective configurable class-parameter:
|
||||
--Class.parameter=value
|
||||
This line is evaluated in Python, so simple expressions are allowed.
|
||||
For instance, to set `C.a=[0,1,2]`, you may type this:
|
||||
--C.a='range(3)'
|
||||
""".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 )
|
||||
|
||||
|
||||
def catch_config_error(method):
|
||||
"""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.
|
||||
"""
|
||||
@functools.wraps(method)
|
||||
def inner(app, *args, **kwargs):
|
||||
try:
|
||||
return method(app, *args, **kwargs)
|
||||
except (TraitError, ArgumentError) as e:
|
||||
app.log.fatal("Bad config encountered during initialization: %s", e)
|
||||
app.log.debug("Config at the time: %s", app.config)
|
||||
app.exit(1)
|
||||
|
||||
return inner
|
||||
|
||||
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('application')
|
||||
|
||||
# The description of the application that is printed at the beginning
|
||||
# of the help.
|
||||
description = Unicode('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, classes=None):
|
||||
"""Iterate through configurable classes, including configurable parents
|
||||
|
||||
:param classes:
|
||||
The list of classes to iterate; if not set, uses :attr:`classes`.
|
||||
|
||||
Children should always be after parents, and each class should only be
|
||||
yielded once.
|
||||
"""
|
||||
if classes is None:
|
||||
classes = self.classes
|
||||
|
||||
seen = set()
|
||||
for c in 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('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, str):
|
||||
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._get_log_handler()
|
||||
if not _log_handler:
|
||||
warnings.warn(
|
||||
f"No Handler found on {self.log}, setting log_format will have no effect",
|
||||
RuntimeWarning,
|
||||
)
|
||||
return
|
||||
_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
|
||||
#: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`.
|
||||
#: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text).
|
||||
aliases = {'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 = {
|
||||
'debug': ({
|
||||
'Application': {
|
||||
'log_level': logging.DEBUG,
|
||||
},
|
||||
}, "Set log-level to debug, for the most verbose logging."),
|
||||
'show-config': ({
|
||||
'Application': {
|
||||
'show_config': True,
|
||||
},
|
||||
}, "Show the application's configuration (human-readable format)"),
|
||||
'show-config-json': ({
|
||||
'Application': {
|
||||
'show_config_json': True,
|
||||
},
|
||||
}, "Show the application's configuration (json format)"),
|
||||
}
|
||||
|
||||
# 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()
|
||||
|
||||
show_config = Bool(
|
||||
help="Instead of starting the Application, dump configuration to stdout"
|
||||
).tag(config=True)
|
||||
|
||||
show_config_json = Bool(
|
||||
help="Instead of starting the Application, dump configuration to stdout (as JSON)"
|
||||
).tag(config=True)
|
||||
|
||||
@observe('show_config_json')
|
||||
def _show_config_json_changed(self, change):
|
||||
self.show_config = change.new
|
||||
|
||||
@observe('show_config')
|
||||
def _show_config_changed(self, change):
|
||||
if change.new:
|
||||
self._save_start = self.start
|
||||
self.start = self.start_show_config
|
||||
|
||||
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
|
||||
self.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: %r', 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 start_show_config(self):
|
||||
"""start function used when show_config is True"""
|
||||
config = self.config.copy()
|
||||
# exclude show_config flags from displayed config
|
||||
for cls in self.__class__.mro():
|
||||
if cls.__name__ in config:
|
||||
cls_config = config[cls.__name__]
|
||||
cls_config.pop('show_config', None)
|
||||
cls_config.pop('show_config_json', None)
|
||||
|
||||
if self.show_config_json:
|
||||
json.dump(config, sys.stdout,
|
||||
indent=1, sort_keys=True, default=repr)
|
||||
# add trailing newline
|
||||
sys.stdout.write('\n')
|
||||
return
|
||||
|
||||
if self._loaded_config_files:
|
||||
print("Loaded config files:")
|
||||
for f in self._loaded_config_files:
|
||||
print(' ' + f)
|
||||
print()
|
||||
|
||||
for classname in sorted(config):
|
||||
class_config = config[classname]
|
||||
if not class_config:
|
||||
continue
|
||||
print(classname)
|
||||
pformat_kwargs = dict(indent=4, compact=True)
|
||||
|
||||
for traitname in sorted(class_config):
|
||||
value = class_config[traitname]
|
||||
print(' .{} = {}'.format(
|
||||
traitname,
|
||||
pprint.pformat(value, **pformat_kwargs),
|
||||
))
|
||||
|
||||
def print_alias_help(self):
|
||||
"""Print the alias parts of the help."""
|
||||
print('\n'.join(self.emit_alias_help()))
|
||||
|
||||
def emit_alias_help(self):
|
||||
"""Yield the lines for alias part of the help."""
|
||||
if not self.aliases:
|
||||
return
|
||||
|
||||
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():
|
||||
try:
|
||||
if isinstance(longname, tuple):
|
||||
longname, fhelp = longname
|
||||
else:
|
||||
fhelp = None
|
||||
classname, traitname = longname.split('.')[-2:]
|
||||
longname = classname + '.' + traitname
|
||||
cls = classdict[classname]
|
||||
|
||||
trait = cls.class_traits(config=True)[traitname]
|
||||
fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines()
|
||||
|
||||
if not isinstance(alias, tuple):
|
||||
alias = (alias, )
|
||||
alias = sorted(alias, key=len)
|
||||
alias = ', '.join(('--%s' if len(m) > 1 else '-%s') % m
|
||||
for m in alias)
|
||||
|
||||
# reformat first line
|
||||
fhelp[0] = fhelp[0].replace('--' + longname, alias)
|
||||
for l in fhelp:
|
||||
yield l
|
||||
yield indent("Equivalent to: [--%s]" % longname)
|
||||
except Exception as ex:
|
||||
self.log.error('Failed collecting help-message for alias %r, due to: %s',
|
||||
alias, ex)
|
||||
raise
|
||||
|
||||
def print_flag_help(self):
|
||||
"""Print the flag part of the help."""
|
||||
print('\n'.join(self.emit_flag_help()))
|
||||
|
||||
def emit_flag_help(self):
|
||||
"""Yield the lines for the flag part of the help."""
|
||||
if not self.flags:
|
||||
return
|
||||
|
||||
for flags, (cfg, fhelp) in self.flags.items():
|
||||
try:
|
||||
if not isinstance(flags, tuple):
|
||||
flags = (flags, )
|
||||
flags = sorted(flags, key=len)
|
||||
flags = ', '.join(('--%s' if len(m) > 1 else '-%s') % m
|
||||
for m in flags)
|
||||
yield flags
|
||||
yield indent(dedent(fhelp.strip()))
|
||||
cfg_list = ' '.join('--%s.%s=%s' %(clname, prop, val)
|
||||
for clname, props_dict
|
||||
in cfg.items()
|
||||
for prop, val in props_dict.items())
|
||||
cfg_txt = "Equivalent to: [%s]" % cfg_list
|
||||
yield indent(dedent(cfg_txt))
|
||||
except Exception as ex:
|
||||
self.log.error('Failed collecting help-message for flag %r, due to: %s',
|
||||
flags, ex)
|
||||
raise
|
||||
|
||||
def print_options(self):
|
||||
"""Print the options part of the help."""
|
||||
print('\n'.join(self.emit_options_help()))
|
||||
|
||||
def emit_options_help(self):
|
||||
"""Yield the lines for the options part of the help."""
|
||||
if not self.flags and not self.aliases:
|
||||
return
|
||||
header = 'Options'
|
||||
yield header
|
||||
yield '=' * len(header)
|
||||
for p in wrap_paragraphs(self.option_description):
|
||||
yield p
|
||||
yield ''
|
||||
|
||||
for l in self.emit_flag_help():
|
||||
yield l
|
||||
for l in self.emit_alias_help():
|
||||
yield l
|
||||
yield ''
|
||||
|
||||
def print_subcommands(self):
|
||||
"""Print the subcommand part of the help."""
|
||||
print('\n'.join(self.emit_subcommands_help()))
|
||||
|
||||
def emit_subcommands_help(self):
|
||||
"""Yield the lines for the subcommand part of the help."""
|
||||
if not self.subcommands:
|
||||
return
|
||||
|
||||
header = "Subcommands"
|
||||
yield header
|
||||
yield '=' * len(header)
|
||||
for p in wrap_paragraphs(self.subcommand_description.format(
|
||||
app=self.name)):
|
||||
yield p
|
||||
yield ''
|
||||
for subc, (cls, help) in self.subcommands.items():
|
||||
yield subc
|
||||
if help:
|
||||
yield indent(dedent(help.strip()))
|
||||
yield ''
|
||||
|
||||
def emit_help_epilogue(self, classes):
|
||||
"""Yield the very bottom lines of the help message.
|
||||
|
||||
If classes=False (the default), print `--help-all` msg.
|
||||
"""
|
||||
if not classes:
|
||||
yield "To see all available configurables, use `--help-all`."
|
||||
yield ''
|
||||
|
||||
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.
|
||||
"""
|
||||
print('\n'.join(self.emit_help(classes=classes)))
|
||||
|
||||
def emit_help(self, classes=False):
|
||||
"""Yield the help-lines for each Configurable class in self.classes.
|
||||
|
||||
If classes=False (the default), only flags and aliases are printed.
|
||||
"""
|
||||
for l in self.emit_description():
|
||||
yield l
|
||||
for l in self.emit_subcommands_help():
|
||||
yield l
|
||||
for l in self.emit_options_help():
|
||||
yield l
|
||||
|
||||
if classes:
|
||||
help_classes = self._classes_with_config_traits()
|
||||
if help_classes:
|
||||
yield "Class options"
|
||||
yield "============="
|
||||
for p in wrap_paragraphs(self.keyvalue_description):
|
||||
yield p
|
||||
yield ''
|
||||
|
||||
for cls in help_classes:
|
||||
yield cls.class_get_help()
|
||||
yield ''
|
||||
for l in self.emit_examples():
|
||||
yield l
|
||||
|
||||
for l in self.emit_help_epilogue(classes):
|
||||
yield l
|
||||
|
||||
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."""
|
||||
print('\n'.join(self.emit_description()))
|
||||
|
||||
def emit_description(self):
|
||||
"""Yield lines with the application description."""
|
||||
for p in wrap_paragraphs(self.description or self.__doc__):
|
||||
yield p
|
||||
yield ''
|
||||
|
||||
def print_examples(self):
|
||||
"""Print usage and examples (see `emit_examples()`). """
|
||||
print('\n'.join(self.emit_examples()))
|
||||
|
||||
def emit_examples(self):
|
||||
"""Yield lines with the 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:
|
||||
yield "Examples"
|
||||
yield "--------"
|
||||
yield ''
|
||||
yield indent(dedent(self.examples.strip()))
|
||||
yield ''
|
||||
|
||||
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, _ = self.subcommands.get(subc)
|
||||
|
||||
if isinstance(subapp, str):
|
||||
subapp = import_item(subapp)
|
||||
|
||||
## Cannot issubclass() on a non-type (SOhttp://stackoverflow.com/questions/8692430)
|
||||
if isinstance(subapp, type) and issubclass(subapp, Application):
|
||||
# Clear existing instances before...
|
||||
self.__class__.clear_instance()
|
||||
# instantiating subapp...
|
||||
self.subapp = subapp.instance(parent=self)
|
||||
elif callable(subapp):
|
||||
# or ask factory to create it...
|
||||
self.subapp = subapp(self)
|
||||
else:
|
||||
raise AssertionError("Invalid mappings for subcommand '%s'!" % subc)
|
||||
|
||||
# ... and finally initialize subapp.
|
||||
self.subapp.initialize(argv)
|
||||
|
||||
def flatten_flags(self):
|
||||
"""Flatten flags and aliases for loaders, 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.
|
||||
Also, loaders expect ``(key: longname)`` and not ````key: (longname, help)`` items.
|
||||
|
||||
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, longname in self.aliases.items():
|
||||
if isinstance(longname, tuple):
|
||||
longname, _ = longname
|
||||
cls, trait = longname.split('.', 1)
|
||||
children = mro_tree[cls]
|
||||
if len(children) == 1:
|
||||
# exactly one descendent, promote alias
|
||||
cls = children[0]
|
||||
if not isinstance(aliases, tuple):
|
||||
alias = (alias, )
|
||||
for al in alias:
|
||||
aliases[al] = '.'.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]
|
||||
|
||||
if cls in newflag:
|
||||
newflag[cls].update(subdict)
|
||||
else:
|
||||
newflag[cls] = subdict
|
||||
|
||||
if not isinstance(key, tuple):
|
||||
key = (key, )
|
||||
for k in key:
|
||||
flags[k] = (newflag, help)
|
||||
return flags, aliases
|
||||
|
||||
def _create_loader(self, argv, aliases, flags, classes):
|
||||
return KVArgParseConfigLoader(argv, aliases, flags, classes=classes,
|
||||
log=self.log)
|
||||
|
||||
@catch_config_error
|
||||
def parse_command_line(self, argv=None):
|
||||
"""Parse the command line arguments."""
|
||||
assert not isinstance(argv, str)
|
||||
argv = sys.argv[1:] if argv is None else argv
|
||||
self.argv = [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()
|
||||
classes = tuple(self._classes_with_config_traits())
|
||||
loader = self._create_loader(argv, aliases, flags, classes=classes)
|
||||
try:
|
||||
self.cli_config = deepcopy(loader.load_config())
|
||||
except SystemExit:
|
||||
# traitlets 5: no longer print help output on error
|
||||
# help output is huge, and comes after the error
|
||||
raise
|
||||
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_with_config_traits(self, classes=None):
|
||||
"""
|
||||
Yields only classes with configurable traits, and their subclasses.
|
||||
|
||||
:param classes:
|
||||
The list of classes to iterate; if not set, uses :attr:`classes`.
|
||||
|
||||
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.
|
||||
"""
|
||||
if classes is None:
|
||||
classes = self.classes
|
||||
|
||||
cls_to_config = OrderedDict( (cls, bool(cls.class_own_traits(config=True)))
|
||||
for cls
|
||||
in self._classes_inc_parents(classes))
|
||||
|
||||
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, classes=None):
|
||||
"""generate default config file from Configurables"""
|
||||
lines = ["# Configuration file for %s." % self.name]
|
||||
lines.append('')
|
||||
classes = self.classes if classes is None else classes
|
||||
config_classes = list(self._classes_with_config_traits(classes))
|
||||
for cls in config_classes:
|
||||
lines.append(cls.class_config_section(config_classes))
|
||||
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
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
default_aliases = Application.aliases
|
||||
default_flags = Application.flags
|
||||
|
||||
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()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Application.launch_instance()
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
"""A base class for objects that are configurable."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from .loader import Config, LazyConfigValue, DeferredConfig, _is_section_key
|
||||
from traitlets.traitlets import (
|
||||
Any,
|
||||
HasTraits,
|
||||
Instance,
|
||||
Container,
|
||||
Dict,
|
||||
observe,
|
||||
observe_compat,
|
||||
default,
|
||||
validate,
|
||||
)
|
||||
from traitlets.utils.text import indent, wrap_paragraphs
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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)
|
||||
|
||||
# record traits set by config
|
||||
config_override_names = set()
|
||||
def notice_config_override(change):
|
||||
"""Record traits set by both config and kwargs.
|
||||
|
||||
They will need to be overridden again after loading config.
|
||||
"""
|
||||
if change.name in kwargs:
|
||||
config_override_names.add(change.name)
|
||||
self.observe(notice_config_override)
|
||||
|
||||
# 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)
|
||||
self.unobserve(notice_config_override)
|
||||
|
||||
for name in config_override_names:
|
||||
setattr(self, name, kwargs[name])
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# 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)
|
||||
elif isinstance(config_value, DeferredConfig):
|
||||
# DeferredConfig tends to come from CLI/environment variables
|
||||
config_value = config_value.get_value(traits[name])
|
||||
# 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 = "Config option `{option}` not recognized by `{klass}`.".format(
|
||||
option=name, klass=self.__class__.__name__)
|
||||
|
||||
if len(matches) == 1:
|
||||
msg += " 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 = []
|
||||
base_classes = ', '.join(p.__name__ for p in cls.__bases__)
|
||||
final_help.append('%s(%s) options' % (cls.__name__, base_classes))
|
||||
final_help.append(len(final_help[0])*'-')
|
||||
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, helptext=None):
|
||||
"""Get the helptext string for a single trait.
|
||||
|
||||
:param inst:
|
||||
If given, it's current trait values will be used in place of
|
||||
the class default.
|
||||
:param helptext:
|
||||
If not given, uses the `help` attribute of the current trait.
|
||||
"""
|
||||
assert inst is None or isinstance(inst, cls)
|
||||
lines = []
|
||||
header = "--%s.%s" % (cls.__name__, trait.name)
|
||||
if isinstance(trait, (Container, Dict)):
|
||||
multiplicity = trait.metadata.get('multiplicity', 'append')
|
||||
if isinstance(trait, Dict):
|
||||
sample_value = '<key-1>=<value-1>'
|
||||
else:
|
||||
sample_value = '<%s-item-1>' % trait.__class__.__name__.lower()
|
||||
if multiplicity == 'append':
|
||||
header = "%s=%s..." % (header, sample_value)
|
||||
else:
|
||||
header = "%s %s..." % (header, sample_value)
|
||||
else:
|
||||
header = '%s=<%s>' % (header, trait.__class__.__name__)
|
||||
#header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
|
||||
lines.append(header)
|
||||
|
||||
if helptext is None:
|
||||
helptext = trait.help
|
||||
if helptext != '':
|
||||
helptext = '\n'.join(wrap_paragraphs(helptext, 76))
|
||||
lines.append(indent(helptext))
|
||||
|
||||
if 'Enum' in trait.__class__.__name__:
|
||||
# include Enum choices
|
||||
lines.append(indent('Choices: %s' % trait.info()))
|
||||
|
||||
if inst is not None:
|
||||
lines.append(indent("Current: %r" % (getattr(inst, trait.name),)))
|
||||
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))
|
||||
|
||||
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 _defining_class(cls, trait, classes):
|
||||
"""Get the class that defines a trait
|
||||
|
||||
For reducing redundant help output in config files.
|
||||
Returns the current class if:
|
||||
- the trait is defined on this class, or
|
||||
- the class where it is defined would not be in the config file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trait : Trait
|
||||
The trait to look for
|
||||
classes : list
|
||||
The list of other classes to consider for redundancy.
|
||||
Will return `cls` even if it is not defined on `cls`
|
||||
if the defining class is not in `classes`.
|
||||
"""
|
||||
defining_cls = cls
|
||||
for parent in cls.mro():
|
||||
if issubclass(parent, Configurable) and \
|
||||
parent in classes and \
|
||||
parent.class_own_traits(config=True).get(trait.name, None) is trait:
|
||||
defining_cls = parent
|
||||
return defining_cls
|
||||
|
||||
@classmethod
|
||||
def class_config_section(cls, classes=None):
|
||||
"""Get the config section for this class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
classes : list, optional
|
||||
The list of other classes in the config file.
|
||||
Used to reduce redundant information.
|
||||
"""
|
||||
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__
|
||||
if issubclass(p, Configurable)
|
||||
)
|
||||
|
||||
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_traits(config=True).items()):
|
||||
default_repr = trait.default_value_repr()
|
||||
|
||||
if classes:
|
||||
defining_class = cls._defining_class(trait, classes)
|
||||
else:
|
||||
defining_class = cls
|
||||
if defining_class is cls:
|
||||
# cls owns the trait, show full help
|
||||
if trait.help:
|
||||
lines.append(c(trait.help))
|
||||
if 'Enum' in type(trait).__name__:
|
||||
# include Enum choices
|
||||
lines.append('# Choices: %s' % trait.info())
|
||||
lines.append('# Default: %s' % default_repr)
|
||||
else:
|
||||
# Trait appears multiple times and isn't defined here.
|
||||
# Truncate help to first line + "See also Original.trait"
|
||||
if trait.help:
|
||||
lines.append(c(trait.help.split('\n', 1)[0]))
|
||||
lines.append('# See also: %s.%s' % (defining_class.__name__, name))
|
||||
|
||||
lines.append('# c.%s.%s = %s' % (cls.__name__, name, default_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_traits(config=True).items()):
|
||||
ttype = trait.__class__.__name__
|
||||
|
||||
termline = classname + '.' + trait.name
|
||||
|
||||
# Choices or type
|
||||
if 'Enum' in ttype:
|
||||
# include Enum choices
|
||||
termline += ' : ' + trait.info_rst()
|
||||
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(indent("Default: ``%s``" % dvr))
|
||||
lines.append("")
|
||||
|
||||
help = trait.help or 'No description'
|
||||
lines.append(indent(dedent(help)))
|
||||
|
||||
# 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 = Any(help="Logger or LoggerAdapter instance")
|
||||
|
||||
@validate("log")
|
||||
def _validate_log(self, proposal):
|
||||
if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)):
|
||||
# warn about unsupported type, but be lenient to allow for duck typing
|
||||
warnings.warn(
|
||||
f"{self.__class__.__name__}.log should be a Logger or LoggerAdapter,"
|
||||
f" got {proposal.value}."
|
||||
)
|
||||
return proposal.value
|
||||
|
||||
@default("log")
|
||||
def _log_default(self):
|
||||
if isinstance(self.parent, LoggingConfigurable):
|
||||
return self.parent.log
|
||||
from traitlets import log
|
||||
return log.get_logger()
|
||||
|
||||
def _get_log_handler(self):
|
||||
"""Return the default Handler
|
||||
|
||||
Returns None if none can be found
|
||||
"""
|
||||
logger = self.log
|
||||
if isinstance(logger, logging.LoggerAdapter):
|
||||
logger = logger.logger
|
||||
if not getattr(logger, "handlers", None):
|
||||
# no handlers attribute or empty handlers list
|
||||
return None
|
||||
return logger.handlers[0]
|
||||
|
||||
|
||||
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(
|
||||
"An incompatible sibling of '%s' is already instanciated"
|
||||
" as singleton: %s" % (cls.__name__, type(cls._instance).__name__)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def initialized(cls):
|
||||
"""Has an instance been created?"""
|
||||
return hasattr(cls, "_instance") and cls._instance is not None
|
||||
|
||||
|
||||
|
||||
1090
.venv/lib/python3.8/site-packages/traitlets/config/loader.py
Normal file
1090
.venv/lib/python3.8/site-packages/traitlets/config/loader.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,84 @@
|
|||
"""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 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()
|
||||
|
||||
f = open(filename, 'w', encoding='utf-8')
|
||||
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
|
||||
161
.venv/lib/python3.8/site-packages/traitlets/config/sphinxdoc.py
Normal file
161
.venv/lib/python3.8/site-packages/traitlets/config/sphinxdoc.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""Machinery for documenting traitlets config options with Sphinx.
|
||||
|
||||
This includes:
|
||||
|
||||
- A Sphinx extension defining directives and roles for config options.
|
||||
- A function to generate an rst file given an Application instance.
|
||||
|
||||
To make this documentation, first set this module as an extension in Sphinx's
|
||||
conf.py::
|
||||
|
||||
extensions = [
|
||||
# ...
|
||||
'traitlets.config.sphinxdoc',
|
||||
]
|
||||
|
||||
Autogenerate the config documentation by running code like this before
|
||||
Sphinx builds::
|
||||
|
||||
from traitlets.config.sphinxdoc import write_doc
|
||||
from myapp import MyApplication
|
||||
|
||||
writedoc('config/options.rst', # File to write
|
||||
'MyApp config options', # Title
|
||||
MyApplication()
|
||||
)
|
||||
|
||||
The generated rST syntax looks like this::
|
||||
|
||||
.. configtrait:: Application.log_datefmt
|
||||
|
||||
Description goes here.
|
||||
|
||||
Cross reference like this: :configtrait:`Application.log_datefmt`.
|
||||
"""
|
||||
from traitlets import Undefined
|
||||
from traitlets.utils.text import indent
|
||||
from collections import defaultdict
|
||||
|
||||
from textwrap import indent as _indent, dedent
|
||||
|
||||
|
||||
def setup(app):
|
||||
"""Registers the Sphinx extension.
|
||||
|
||||
You shouldn't need to call this directly; configure Sphinx to use this
|
||||
module instead.
|
||||
"""
|
||||
app.add_object_type('configtrait', 'configtrait', objname='Config option')
|
||||
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
|
||||
return metadata
|
||||
|
||||
def interesting_default_value(dv):
|
||||
if (dv is None) or (dv is Undefined):
|
||||
return False
|
||||
if isinstance(dv, (str, list, tuple, dict, set)):
|
||||
return bool(dv)
|
||||
return True
|
||||
|
||||
def format_aliases(aliases):
|
||||
fmted = []
|
||||
for a in aliases:
|
||||
dashes = '-' if len(a) == 1 else '--'
|
||||
fmted.append('``%s%s``' % (dashes, a))
|
||||
return ', '.join(fmted)
|
||||
|
||||
def class_config_rst_doc(cls, trait_aliases):
|
||||
"""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_traits(config=True).items()):
|
||||
ttype = trait.__class__.__name__
|
||||
|
||||
fullname = classname + '.' + trait.name
|
||||
lines += ['.. configtrait:: ' + fullname,
|
||||
''
|
||||
]
|
||||
|
||||
help = trait.help.rstrip() or "No description"
|
||||
lines.append(indent(dedent(help)) + "\n")
|
||||
|
||||
# Choices or type
|
||||
if 'Enum' in ttype:
|
||||
# include Enum choices
|
||||
lines.append(
|
||||
indent(":options: " + ", ".join("``%r``" % x for x in trait.values))
|
||||
)
|
||||
else:
|
||||
lines.append(indent(":trait type: " + ttype))
|
||||
|
||||
# Default value
|
||||
# Ignore boring default values like None, [] or ''
|
||||
if interesting_default_value(trait.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(indent(":default: ``%s``" % dvr))
|
||||
|
||||
# Command line aliases
|
||||
if trait_aliases[fullname]:
|
||||
fmt_aliases = format_aliases(trait_aliases[fullname])
|
||||
lines.append(indent(":CLI option: " + fmt_aliases))
|
||||
|
||||
# Blank line
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def reverse_aliases(app):
|
||||
"""Produce a mapping of trait names to lists of command line aliases.
|
||||
"""
|
||||
res = defaultdict(list)
|
||||
for alias, trait in app.aliases.items():
|
||||
res[trait].append(alias)
|
||||
|
||||
# Flags also often act as aliases for a boolean trait.
|
||||
# Treat flags which set one trait to True as aliases.
|
||||
for flag, (cfg, _) in app.flags.items():
|
||||
if len(cfg) == 1:
|
||||
classname = list(cfg)[0]
|
||||
cls_cfg = cfg[classname]
|
||||
if len(cls_cfg) == 1:
|
||||
traitname = list(cls_cfg)[0]
|
||||
if cls_cfg[traitname] is True:
|
||||
res[classname+'.'+traitname].append(flag)
|
||||
|
||||
return res
|
||||
|
||||
def write_doc(path, title, app, preamble=None):
|
||||
"""Write a rst file documenting config options for a traitlets application.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The file to be written
|
||||
title : str
|
||||
The human-readable title of the document
|
||||
app : traitlets.config.Application
|
||||
An instance of the application class to be documented
|
||||
preamble : str
|
||||
Extra text to add just after the title (optional)
|
||||
"""
|
||||
trait_aliases = reverse_aliases(app)
|
||||
with open(path, 'w') as f:
|
||||
f.write(title + '\n')
|
||||
f.write(('=' * len(title)) + '\n')
|
||||
f.write('\n')
|
||||
if preamble is not None:
|
||||
f.write(preamble + '\n\n')
|
||||
|
||||
for c in app._classes_inc_parents():
|
||||
f.write(class_config_rst_doc(c, trait_aliases))
|
||||
f.write('\n')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,767 @@
|
|||
# coding: utf-8
|
||||
"""
|
||||
Tests for traitlets.config.application.Application
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest import TestCase
|
||||
|
||||
import pytest
|
||||
from pytest import mark
|
||||
|
||||
from traitlets import (
|
||||
Bool,
|
||||
Bytes,
|
||||
Dict,
|
||||
HasTraits,
|
||||
Integer,
|
||||
List,
|
||||
Set,
|
||||
Tuple,
|
||||
Unicode,
|
||||
)
|
||||
from traitlets.config.application import Application
|
||||
from traitlets.config.configurable import Configurable
|
||||
from traitlets.config.loader import Config
|
||||
from traitlets.tests.utils import (
|
||||
check_help_all_output,
|
||||
check_help_output,
|
||||
get_output_error_code,
|
||||
)
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
pjoin = os.path.join
|
||||
|
||||
|
||||
class Foo(Configurable):
|
||||
|
||||
i = Integer(0, help="""
|
||||
The integer i.
|
||||
|
||||
Details about i.
|
||||
""").tag(config=True)
|
||||
j = Integer(1, help="The integer j.").tag(config=True)
|
||||
name = Unicode('Brian', help="First name.").tag(config=True)
|
||||
la = List([]).tag(config=True)
|
||||
li = List(Integer()).tag(config=True)
|
||||
fdict = Dict().tag(config=True, multiplicity='+')
|
||||
|
||||
|
||||
class Bar(Configurable):
|
||||
|
||||
b = Integer(0, help="The integer b.").tag(config=True)
|
||||
enabled = Bool(True, help="Enable bar.").tag(config=True)
|
||||
tb = Tuple(()).tag(config=True, multiplicity='*')
|
||||
aset = Set().tag(config=True, multiplicity='+')
|
||||
bdict = Dict().tag(config=True)
|
||||
idict = Dict(value_trait=Integer()).tag(config=True)
|
||||
key_dict = Dict(per_key_traits={'i': Integer(), 'b': Bytes()}).tag(config=True)
|
||||
|
||||
|
||||
class MyApp(Application):
|
||||
|
||||
name = Unicode('myapp')
|
||||
running = Bool(False, help="Is the app running?").tag(config=True)
|
||||
classes = List([Bar, Foo])
|
||||
config_file = Unicode('', help="Load this config file").tag(config=True)
|
||||
|
||||
warn_tpyo = Unicode("yes the name is wrong on purpose", config=True,
|
||||
help="Should print a warning if `MyApp.warn-typo=...` command is passed")
|
||||
|
||||
aliases = {}
|
||||
aliases.update(Application.aliases)
|
||||
aliases.update({
|
||||
('fooi', 'i') : 'Foo.i',
|
||||
('j', 'fooj') : ('Foo.j', "`j` terse help msg"),
|
||||
'name' : 'Foo.name',
|
||||
'la': 'Foo.la',
|
||||
'li': 'Foo.li',
|
||||
'tb': 'Bar.tb',
|
||||
'D': 'Bar.bdict',
|
||||
'enabled' : 'Bar.enabled',
|
||||
'enable' : 'Bar.enabled',
|
||||
'log-level' : 'Application.log_level',
|
||||
})
|
||||
|
||||
flags = {}
|
||||
flags.update(Application.flags)
|
||||
flags.update({('enable', 'e'):
|
||||
({'Bar': {'enabled' : True}},
|
||||
"Set Bar.enabled to True"),
|
||||
('d', '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)
|
||||
|
||||
|
||||
def class_to_names(classes):
|
||||
return [klass.__name__ for klass in classes]
|
||||
|
||||
|
||||
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_no_eval_cli_text(self):
|
||||
app = MyApp()
|
||||
app.initialize(['--Foo.name=1'])
|
||||
app.init_foo()
|
||||
assert app.foo.name == '1'
|
||||
|
||||
def test_basic(self):
|
||||
app = MyApp()
|
||||
self.assertEqual(app.name, 'myapp')
|
||||
self.assertEqual(app.running, False)
|
||||
self.assertEqual(app.classes, [MyApp, Bar, Foo])
|
||||
self.assertEqual(app.config_file, '')
|
||||
|
||||
def test_mro_discovery(self):
|
||||
app = MyApp()
|
||||
|
||||
self.assertSequenceEqual(class_to_names(app._classes_with_config_traits()),
|
||||
['Application', 'MyApp', 'Bar', 'Foo'])
|
||||
self.assertSequenceEqual(class_to_names(app._classes_inc_parents()),
|
||||
['Configurable', 'LoggingConfigurable', 'SingletonConfigurable',
|
||||
'Application', 'MyApp', 'Bar', 'Foo'])
|
||||
|
||||
self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Application])),
|
||||
['Application'])
|
||||
self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Application])),
|
||||
['Configurable', 'LoggingConfigurable', 'SingletonConfigurable',
|
||||
'Application'])
|
||||
|
||||
self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])),
|
||||
['Foo'])
|
||||
self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Bar])),
|
||||
['Configurable', 'Bar'])
|
||||
|
||||
class MyApp2(Application): # no defined `classes` attr
|
||||
pass
|
||||
|
||||
self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])),
|
||||
['Foo'])
|
||||
self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Bar])),
|
||||
['Configurable', 'Bar'])
|
||||
|
||||
|
||||
def test_config(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line([
|
||||
"--i=10",
|
||||
"--Foo.j=10",
|
||||
"--enable=False",
|
||||
"--log-level=50",
|
||||
])
|
||||
config = app.config
|
||||
print(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_seq_args(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line("--li 1 --li 3 --la 1 --tb AB 2 --Foo.la=ab --Bar.aset S1 --Bar.aset S2 --Bar.aset S1".split())
|
||||
assert app.extra_args == ["2"]
|
||||
config = app.config
|
||||
assert config.Foo.li == [1, 3]
|
||||
assert config.Foo.la == ["1", "ab"]
|
||||
assert config.Bar.tb == ("AB",)
|
||||
self.assertEqual(config.Bar.aset, {"S1", "S2"})
|
||||
app.init_foo()
|
||||
assert app.foo.li == [1, 3]
|
||||
assert app.foo.la == ['1', 'ab']
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.aset, {'S1', 'S2'})
|
||||
assert app.bar.tb == ('AB',)
|
||||
|
||||
def test_config_dict_args(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(
|
||||
"--Foo.fdict a=1 --Foo.fdict b=b --Foo.fdict c=3 "
|
||||
"--Bar.bdict k=1 -D=a=b -D 22=33 "
|
||||
"--Bar.idict k=1 --Bar.idict b=2 --Bar.idict c=3 "
|
||||
.split())
|
||||
fdict = {'a': '1', 'b': 'b', 'c': '3'}
|
||||
bdict = {'k': '1', 'a': 'b', '22': '33'}
|
||||
idict = {'k': 1, 'b': 2, 'c': 3}
|
||||
config = app.config
|
||||
assert config.Bar.idict == idict
|
||||
self.assertDictEqual(config.Foo.fdict, fdict)
|
||||
self.assertDictEqual(config.Bar.bdict, bdict)
|
||||
app.init_foo()
|
||||
self.assertEqual(app.foo.fdict, fdict)
|
||||
app.init_bar()
|
||||
assert app.bar.idict == idict
|
||||
self.assertEqual(app.bar.bdict, bdict)
|
||||
|
||||
def test_config_propagation(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--i=10","--Foo.j=10","--enable=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', 'some help')}
|
||||
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_cli_allow_none(self):
|
||||
class App(Application):
|
||||
aliases = {"opt": "App.opt"}
|
||||
opt = Unicode(allow_none=True, config=True)
|
||||
|
||||
app = App()
|
||||
app.parse_command_line(["--opt=None"])
|
||||
assert app.opt is None
|
||||
|
||||
def test_flags(self):
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--disable"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
|
||||
app = MyApp()
|
||||
app.parse_command_line(["-d"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, False)
|
||||
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--enable"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, True)
|
||||
|
||||
app = MyApp()
|
||||
app.parse_command_line(["-e"])
|
||||
app.init_bar()
|
||||
self.assertEqual(app.bar.enabled, True)
|
||||
|
||||
def test_flags_help_msg(self):
|
||||
app = MyApp()
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
app.print_flag_help()
|
||||
hmsg = stdout.getvalue()
|
||||
self.assertRegex(hmsg, "(?<!-)-e, --enable\\b")
|
||||
self.assertRegex(hmsg, "(?<!-)-d, --disable\\b")
|
||||
self.assertIn("Equivalent to: [--Bar.enabled=True]", hmsg)
|
||||
self.assertIn("Equivalent to: [--Bar.enabled=False]", hmsg)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
app = MyApp()
|
||||
app.parse_command_line(["--fooi=5", "--fooj=10"])
|
||||
app.init_foo()
|
||||
self.assertEqual(app.foo.i, 5)
|
||||
app.init_foo()
|
||||
self.assertEqual(app.foo.j, 10)
|
||||
|
||||
def test_aliases_help_msg(self):
|
||||
app = MyApp()
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
app.print_alias_help()
|
||||
hmsg = stdout.getvalue()
|
||||
self.assertRegex(hmsg, "(?<!-)-i, --fooi\\b")
|
||||
self.assertRegex(hmsg, "(?<!-)-j, --fooj\\b")
|
||||
self.assertIn("Equivalent to: [--Foo.i]", hmsg)
|
||||
self.assertIn("Equivalent to: [--Foo.j]", hmsg)
|
||||
self.assertIn("Equivalent to: [--Foo.name]", hmsg)
|
||||
|
||||
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', 'args', "--disable"])
|
||||
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'])
|
||||
|
||||
app = MyApp()
|
||||
app.parse_command_line(
|
||||
["--disable", "--la", "-", "-", "--Bar.b=1", "--", "-", "extra"]
|
||||
)
|
||||
self.assertEqual(app.extra_args, ["-", "-", "extra"])
|
||||
|
||||
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 NotInConfig(HasTraits):
|
||||
from_hidden = Unicode('x', help="""From hidden class
|
||||
|
||||
Details about from_hidden.
|
||||
""").tag(config=True)
|
||||
|
||||
class NoTraits(Foo, Bar, NotInConfig):
|
||||
pass
|
||||
|
||||
app = MyApp()
|
||||
app.classes.append(NoTraits)
|
||||
|
||||
conf_txt = app.generate_config_file()
|
||||
print(conf_txt)
|
||||
self.assertIn('The integer b.', conf_txt)
|
||||
self.assertIn('# Foo(Configurable)', conf_txt)
|
||||
self.assertNotIn('# Configurable', conf_txt)
|
||||
self.assertIn('# NoTraits(Foo, Bar)', conf_txt)
|
||||
|
||||
# inherited traits, parent in class list:
|
||||
self.assertIn('# c.NoTraits.i', conf_txt)
|
||||
self.assertIn('# c.NoTraits.j', conf_txt)
|
||||
self.assertIn('# c.NoTraits.n', conf_txt)
|
||||
self.assertIn('# See also: Foo.j', conf_txt)
|
||||
self.assertIn('# See also: Bar.b', conf_txt)
|
||||
self.assertEqual(conf_txt.count('Details about i.'), 1)
|
||||
|
||||
# inherited traits, parent not in class list:
|
||||
self.assertIn("# c.NoTraits.from_hidden", conf_txt)
|
||||
self.assertNotIn('# See also: NotInConfig.', conf_txt)
|
||||
self.assertEqual(conf_txt.count('Details about from_hidden.'), 1)
|
||||
self.assertNotIn("NotInConfig", 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_subcommands_instanciation(self):
|
||||
"""Try all ways to specify how to create sub-apps."""
|
||||
app = Root.instance()
|
||||
app.parse_command_line(['sub1'])
|
||||
|
||||
self.assertIsInstance(app.subapp, Sub1)
|
||||
## Check parent hierarchy.
|
||||
self.assertIs(app.subapp.parent, app)
|
||||
|
||||
Root.clear_instance()
|
||||
Sub1.clear_instance() # Otherwise, replaced spuriously and hierarchy check fails.
|
||||
app = Root.instance()
|
||||
|
||||
app.parse_command_line(['sub1', 'sub2'])
|
||||
self.assertIsInstance(app.subapp, Sub1)
|
||||
self.assertIsInstance(app.subapp.subapp, Sub2)
|
||||
## Check parent hierarchy.
|
||||
self.assertIs(app.subapp.parent, app)
|
||||
self.assertIs(app.subapp.subapp.parent, app.subapp)
|
||||
|
||||
Root.clear_instance()
|
||||
Sub1.clear_instance() # Otherwise, replaced spuriously and hierarchy check fails.
|
||||
app = Root.instance()
|
||||
|
||||
app.parse_command_line(['sub1', 'sub3'])
|
||||
self.assertIsInstance(app.subapp, Sub1)
|
||||
self.assertIsInstance(app.subapp.subapp, Sub3)
|
||||
self.assertTrue(app.subapp.subapp.flag) # Set by factory.
|
||||
## Check parent hierarchy.
|
||||
self.assertIs(app.subapp.parent, app)
|
||||
self.assertIs(app.subapp.subapp.parent, app.subapp) # Set by factory.
|
||||
|
||||
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.assertEqual(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)
|
||||
|
||||
|
||||
def test_cli_multi_scalar(caplog):
|
||||
class App(Application):
|
||||
aliases = {"opt": "App.opt"}
|
||||
opt = Unicode(config=True)
|
||||
|
||||
app = App(log=logging.getLogger())
|
||||
with pytest.raises(SystemExit):
|
||||
app.parse_command_line(["--opt", "1", "--opt", "2"])
|
||||
record = caplog.get_records("call")[-1]
|
||||
message = record.message
|
||||
|
||||
assert "Error loading argument" in message
|
||||
assert "App.opt=['1', '2']" in message
|
||||
assert "opt only accepts one value" in message
|
||||
assert record.levelno == logging.CRITICAL
|
||||
|
||||
|
||||
class Root(Application):
|
||||
subcommands = {
|
||||
'sub1': ('traitlets.config.tests.test_application.Sub1', 'import string'),
|
||||
}
|
||||
|
||||
|
||||
class Sub3(Application):
|
||||
flag = Bool(False)
|
||||
|
||||
|
||||
class Sub2(Application):
|
||||
pass
|
||||
|
||||
|
||||
class Sub1(Application):
|
||||
subcommands = {
|
||||
'sub2': (Sub2, 'Application class'),
|
||||
'sub3': (lambda root: Sub3(parent=root, flag=True), 'factory'),
|
||||
}
|
||||
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
def test_help_all_output():
|
||||
check_help_all_output(__name__)
|
||||
|
||||
|
||||
def test_show_config_cli():
|
||||
out, err, ec = get_output_error_code([sys.executable, '-m', __name__, '--show-config'])
|
||||
assert ec == 0
|
||||
assert 'show_config' not in out
|
||||
|
||||
|
||||
def test_show_config_json_cli():
|
||||
out, err, ec = get_output_error_code([sys.executable, '-m', __name__, '--show-config-json'])
|
||||
assert ec == 0
|
||||
assert 'show_config' not in out
|
||||
|
||||
|
||||
def test_show_config(capsys):
|
||||
cfg = Config()
|
||||
cfg.MyApp.i = 5
|
||||
# don't show empty
|
||||
cfg.OtherApp
|
||||
|
||||
app = MyApp(config=cfg, show_config=True)
|
||||
app.start()
|
||||
out, err = capsys.readouterr()
|
||||
assert 'MyApp' in out
|
||||
assert 'i = 5' in out
|
||||
assert 'OtherApp' not in out
|
||||
|
||||
|
||||
def test_show_config_json(capsys):
|
||||
cfg = Config()
|
||||
cfg.MyApp.i = 5
|
||||
cfg.OtherApp
|
||||
|
||||
app = MyApp(config=cfg, show_config_json=True)
|
||||
app.start()
|
||||
out, err = capsys.readouterr()
|
||||
displayed = json.loads(out)
|
||||
assert Config(displayed) == cfg
|
||||
|
||||
|
||||
def test_deep_alias():
|
||||
from traitlets.config import Application, Configurable
|
||||
from traitlets import Int
|
||||
|
||||
class Foo(Configurable):
|
||||
val = Int(default_value=5).tag(config=True)
|
||||
|
||||
class Bar(Configurable):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.foo = Foo(parent=self)
|
||||
|
||||
class TestApp(Application):
|
||||
name = 'test'
|
||||
|
||||
aliases = {'val': 'Bar.Foo.val'}
|
||||
classes = [Foo, Bar]
|
||||
|
||||
def initialize(self, *args, **kwargs):
|
||||
super().initialize(*args, **kwargs)
|
||||
self.bar = Bar(parent=self)
|
||||
|
||||
app = TestApp()
|
||||
app.initialize(['--val=10'])
|
||||
assert app.bar.foo.val == 10
|
||||
assert len(list(app.emit_alias_help())) > 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# for test_help_output:
|
||||
MyApp.launch_instance()
|
||||
|
|
@ -0,0 +1,680 @@
|
|||
"""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.application import Application
|
||||
from traitlets.config.configurable import (
|
||||
Configurable,
|
||||
LoggingConfigurable,
|
||||
SingletonConfigurable,
|
||||
)
|
||||
from traitlets.log import get_logger
|
||||
from traitlets.traitlets import (
|
||||
Integer, Float, Unicode, List, Dict, Set, Enum, FuzzyEnum,
|
||||
CaselessStrEnum, _deprecations_shown, validate,
|
||||
)
|
||||
|
||||
from traitlets.config.loader import Config
|
||||
|
||||
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 = """MyConfigurable(Configurable) options
|
||||
------------------------------------
|
||||
--MyConfigurable.a=<Integer>
|
||||
The integer a.
|
||||
Default: 1
|
||||
--MyConfigurable.b=<Float>
|
||||
The integer b.
|
||||
Default: 1.0"""
|
||||
|
||||
mc_help_inst="""MyConfigurable(Configurable) options
|
||||
------------------------------------
|
||||
--MyConfigurable.a=<Integer>
|
||||
The integer a.
|
||||
Current: 5
|
||||
--MyConfigurable.b=<Float>
|
||||
The integer b.
|
||||
Current: 4.0"""
|
||||
|
||||
# On Python 3, the Integer trait is a synonym for Int
|
||||
mc_help = mc_help.replace("<Integer>", "<Int>")
|
||||
mc_help_inst = mc_help_inst.replace("<Integer>", "<Int>")
|
||||
|
||||
class Foo(Configurable):
|
||||
a = Integer(0, help="The integer a.").tag(config=True)
|
||||
b = Unicode('nope').tag(config=True)
|
||||
flist = List([]).tag(config=True)
|
||||
fdict = Dict().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)
|
||||
bset = Set([]).tag(config=True, multiplicity='+')
|
||||
bset_values = Set([2,1,5]).tag(config=True, multiplicity='+')
|
||||
bdict = Dict().tag(config=True, multiplicity='+')
|
||||
bdict_values = Dict({1:'a','0':'b',5:'c'}).tag(config=True, multiplicity='+')
|
||||
|
||||
foo_help="""Foo(Configurable) options
|
||||
-------------------------
|
||||
--Foo.a=<Int>
|
||||
The integer a.
|
||||
Default: 0
|
||||
--Foo.b=<Unicode>
|
||||
Default: 'nope'
|
||||
--Foo.fdict=<key-1>=<value-1>...
|
||||
Default: {}
|
||||
--Foo.flist=<list-item-1>...
|
||||
Default: []"""
|
||||
|
||||
bar_help="""Bar(Foo) options
|
||||
----------------
|
||||
--Bar.a=<Int>
|
||||
The integer a.
|
||||
Default: 0
|
||||
--Bar.bdict <key-1>=<value-1>...
|
||||
Default: {}
|
||||
--Bar.bdict_values <key-1>=<value-1>...
|
||||
Default: {1: 'a', '0': 'b', 5: 'c'}
|
||||
--Bar.bset <set-item-1>...
|
||||
Default: set()
|
||||
--Bar.bset_values <set-item-1>...
|
||||
Default: {1, 2, 5}
|
||||
--Bar.c=<Float>
|
||||
The string c.
|
||||
Default: 0.0
|
||||
--Bar.fdict=<key-1>=<value-1>...
|
||||
Default: {}
|
||||
--Bar.flist=<list-item-1>...
|
||||
Default: []"""
|
||||
|
||||
|
||||
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)
|
||||
self.assertEqual(Foo.class_get_help(), foo_help)
|
||||
self.assertEqual(Bar.class_get_help(), bar_help)
|
||||
|
||||
def test_help_inst(self):
|
||||
inst = MyConfigurable(a=5, b=4)
|
||||
self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
|
||||
|
||||
def test_generated_config_enum_comments(self):
|
||||
class MyConf(Configurable):
|
||||
an_enum = Enum('Choice1 choice2'.split(),
|
||||
help="Many choices.").tag(config=True)
|
||||
|
||||
help_str = "Many choices."
|
||||
enum_choices_str = "Choices: any of ['Choice1', 'choice2']"
|
||||
rst_choices_str = "MyConf.an_enum : any of ``'Choice1'``|``'choice2'``"
|
||||
or_none_str = "or None"
|
||||
|
||||
cls_help = MyConf.class_get_help()
|
||||
|
||||
self.assertIn(help_str, cls_help)
|
||||
self.assertIn(enum_choices_str, cls_help)
|
||||
self.assertNotIn(or_none_str, cls_help)
|
||||
|
||||
cls_cfg = MyConf.class_config_section()
|
||||
|
||||
self.assertIn(help_str, cls_cfg)
|
||||
self.assertIn(enum_choices_str, cls_cfg)
|
||||
self.assertNotIn(or_none_str, cls_help)
|
||||
## Check order of Help-msg <--> Choices sections
|
||||
self.assertGreater(cls_cfg.index(enum_choices_str),
|
||||
cls_cfg.index(help_str))
|
||||
|
||||
rst_help = MyConf.class_config_rst_doc()
|
||||
|
||||
self.assertIn(help_str, rst_help)
|
||||
self.assertIn(rst_choices_str, rst_help)
|
||||
self.assertNotIn(or_none_str, rst_help)
|
||||
|
||||
class MyConf2(Configurable):
|
||||
an_enum = Enum('Choice1 choice2'.split(),
|
||||
allow_none=True,
|
||||
default_value='choice2',
|
||||
help="Many choices.").tag(config=True)
|
||||
|
||||
defaults_str = "Default: 'choice2'"
|
||||
|
||||
cls2_msg = MyConf2.class_get_help()
|
||||
|
||||
self.assertIn(help_str, cls2_msg)
|
||||
self.assertIn(enum_choices_str, cls2_msg)
|
||||
self.assertIn(or_none_str, cls2_msg)
|
||||
self.assertIn(defaults_str, cls2_msg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls2_msg.index(defaults_str),
|
||||
cls2_msg.index(enum_choices_str))
|
||||
|
||||
cls2_cfg = MyConf2.class_config_section()
|
||||
|
||||
self.assertIn(help_str, cls2_cfg)
|
||||
self.assertIn(enum_choices_str, cls2_cfg)
|
||||
self.assertIn(or_none_str, cls2_cfg)
|
||||
self.assertIn(defaults_str, cls2_cfg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls2_cfg.index(defaults_str),
|
||||
cls2_cfg.index(enum_choices_str))
|
||||
|
||||
def test_generated_config_strenum_comments(self):
|
||||
help_str = "Many choices."
|
||||
defaults_str = "Default: 'choice2'"
|
||||
or_none_str = "or None"
|
||||
|
||||
class MyConf3(Configurable):
|
||||
an_enum = CaselessStrEnum('Choice1 choice2'.split(),
|
||||
allow_none=True,
|
||||
default_value='choice2',
|
||||
help="Many choices.").tag(config=True)
|
||||
|
||||
enum_choices_str = ("Choices: any of ['Choice1', 'choice2'] "
|
||||
"(case-insensitive)")
|
||||
|
||||
cls3_msg = MyConf3.class_get_help()
|
||||
|
||||
self.assertIn(help_str, cls3_msg)
|
||||
self.assertIn(enum_choices_str, cls3_msg)
|
||||
self.assertIn(or_none_str, cls3_msg)
|
||||
self.assertIn(defaults_str, cls3_msg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls3_msg.index(defaults_str),
|
||||
cls3_msg.index(enum_choices_str))
|
||||
|
||||
cls3_cfg = MyConf3.class_config_section()
|
||||
|
||||
self.assertIn(help_str, cls3_cfg)
|
||||
self.assertIn(enum_choices_str, cls3_cfg)
|
||||
self.assertIn(or_none_str, cls3_cfg)
|
||||
self.assertIn(defaults_str, cls3_cfg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls3_cfg.index(defaults_str),
|
||||
cls3_cfg.index(enum_choices_str))
|
||||
|
||||
class MyConf4(Configurable):
|
||||
an_enum = FuzzyEnum('Choice1 choice2'.split(),
|
||||
allow_none=True,
|
||||
default_value='choice2',
|
||||
help="Many choices.").tag(config=True)
|
||||
|
||||
enum_choices_str = ("Choices: any case-insensitive prefix "
|
||||
"of ['Choice1', 'choice2']")
|
||||
|
||||
cls4_msg = MyConf4.class_get_help()
|
||||
|
||||
self.assertIn(help_str, cls4_msg)
|
||||
self.assertIn(enum_choices_str, cls4_msg)
|
||||
self.assertIn(or_none_str, cls4_msg)
|
||||
self.assertIn(defaults_str, cls4_msg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls4_msg.index(defaults_str),
|
||||
cls4_msg.index(enum_choices_str))
|
||||
|
||||
cls4_cfg = MyConf4.class_config_section()
|
||||
|
||||
self.assertIn(help_str, cls4_cfg)
|
||||
self.assertIn(enum_choices_str, cls4_cfg)
|
||||
self.assertIn(or_none_str, cls4_cfg)
|
||||
self.assertIn(defaults_str, cls4_cfg)
|
||||
## Check order of Default <--> Choices sections
|
||||
self.assertGreater(cls4_cfg.index(defaults_str),
|
||||
cls4_cfg.index(enum_choices_str))
|
||||
|
||||
|
||||
|
||||
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()
|
||||
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 TestLoggingConfigurable(TestCase):
|
||||
|
||||
def test_parent_logger(self):
|
||||
class Parent(LoggingConfigurable): pass
|
||||
class Child(LoggingConfigurable): pass
|
||||
log = get_logger().getChild("TestLoggingConfigurable")
|
||||
|
||||
parent = Parent(log=log)
|
||||
child = Child(parent=parent)
|
||||
self.assertEqual(parent.log, log)
|
||||
self.assertEqual(child.log, log)
|
||||
|
||||
parent = Parent()
|
||||
child = Child(parent=parent, log=log)
|
||||
self.assertEqual(parent.log, get_logger())
|
||||
self.assertEqual(child.log, log)
|
||||
|
||||
def test_parent_not_logging_configurable(self):
|
||||
class Parent(Configurable): pass
|
||||
class Child(LoggingConfigurable): pass
|
||||
parent = Parent()
|
||||
child = Child(parent=parent)
|
||||
self.assertEqual(child.log, get_logger())
|
||||
|
||||
|
||||
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,
|
||||
},
|
||||
'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)
|
||||
|
||||
def test_kwarg_config_priority(self):
|
||||
# a, c set in kwargs
|
||||
# a, b set in config
|
||||
# verify that:
|
||||
# - kwargs are set before config
|
||||
# - kwargs have priority over config
|
||||
class A(Configurable):
|
||||
a = Unicode('default', config=True)
|
||||
b = Unicode('default', config=True)
|
||||
c = Unicode('default', config=True)
|
||||
c_during_config = Unicode('never')
|
||||
@validate('b')
|
||||
def _record_c(self, proposal):
|
||||
# setting b from config records c's value at the time
|
||||
self.c_during_config = self.c
|
||||
return proposal.value
|
||||
|
||||
cfg = Config()
|
||||
cfg.A.a = 'a-config'
|
||||
cfg.A.b = 'b-config'
|
||||
obj = A(a='a-kwarg', c='c-kwarg', config=cfg)
|
||||
assert obj.a == 'a-kwarg'
|
||||
assert obj.b == 'b-config'
|
||||
assert obj.c == 'c-kwarg'
|
||||
assert obj.c_during_config == 'c-kwarg'
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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)
|
||||
|
||||
def test_logger_adapter(self):
|
||||
logger = logging.getLogger("test_logger_adapter")
|
||||
adapter = logging.LoggerAdapter(logger, {"key": "adapted"})
|
||||
|
||||
with self.assertLogs(logger, logging.INFO) as captured:
|
||||
app = Application(log=adapter, log_level=logging.INFO)
|
||||
app.log_format = "%(key)s %(message)s"
|
||||
app.log.info("test message")
|
||||
|
||||
output = "\n".join(captured.output)
|
||||
assert "adapted test message" in output
|
||||
|
|
@ -0,0 +1,754 @@
|
|||
# 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 os
|
||||
import pickle
|
||||
from itertools import chain
|
||||
from tempfile import mkstemp
|
||||
from unittest import TestCase
|
||||
|
||||
import pytest
|
||||
|
||||
from traitlets.config.loader import (
|
||||
Config,
|
||||
LazyConfigValue,
|
||||
PyFileConfigLoader,
|
||||
JSONFileConfigLoader,
|
||||
KeyValueConfigLoader,
|
||||
ArgParseConfigLoader,
|
||||
KVArgParseConfigLoader,
|
||||
)
|
||||
from traitlets import (
|
||||
List,
|
||||
Tuple,
|
||||
Dict,
|
||||
Unicode,
|
||||
Integer,
|
||||
)
|
||||
from traitlets.config import Configurable
|
||||
|
||||
|
||||
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', prefix='μnïcø∂e')
|
||||
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', prefix='μnïcø∂e')
|
||||
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', prefix='μnïcø∂e')
|
||||
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', prefix='μnïcø∂e')
|
||||
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', prefix='μnïcø∂e')
|
||||
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()
|
||||
|
||||
|
||||
def _parse_int_or_str(v):
|
||||
try:
|
||||
return int(v)
|
||||
except:
|
||||
return str(v)
|
||||
|
||||
|
||||
class MyLoader1(ArgParseConfigLoader):
|
||||
def _add_arguments(self, aliases=None, flags=None, classes=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)
|
||||
p.add_argument('--list1', action='append', type=_parse_int_or_str)
|
||||
p.add_argument('--list2', nargs='+', type=int)
|
||||
|
||||
|
||||
class MyLoader2(ArgParseConfigLoader):
|
||||
def _add_arguments(self, aliases=None, flags=None, classes=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')
|
||||
|
||||
def test_list_args(self):
|
||||
cl = MyLoader1()
|
||||
config = cl.load_config('--list1 1 wow --list2 1 2 3 --list1 B'.split())
|
||||
self.assertEqual(list(config.Global.keys()), ['bam'])
|
||||
self.assertEqual(config.Global.bam, 'wow')
|
||||
self.assertEqual(config.list1, [1, 'B'])
|
||||
self.assertEqual(config.list2, [1, 2, 3])
|
||||
|
||||
|
||||
class C(Configurable):
|
||||
str_trait = Unicode(config=True)
|
||||
int_trait = Integer(config=True)
|
||||
list_trait = List(config=True)
|
||||
list_of_ints = List(Integer(), config=True)
|
||||
dict_trait = Dict(config=True)
|
||||
dict_of_ints = Dict(
|
||||
key_trait=Integer(),
|
||||
value_trait=Integer(),
|
||||
config=True,
|
||||
)
|
||||
dict_multi = Dict(
|
||||
key_trait=Unicode(),
|
||||
per_key_traits={
|
||||
"int": Integer(),
|
||||
"str": Unicode(),
|
||||
},
|
||||
config=True,
|
||||
)
|
||||
|
||||
|
||||
class TestKeyValueCL(TestCase):
|
||||
klass = KeyValueConfigLoader
|
||||
|
||||
def test_eval(self):
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config('--C.str_trait=all --C.int_trait=5 --C.list_trait=["hello",5]'.split())
|
||||
c = C(config=config)
|
||||
assert c.str_trait == 'all'
|
||||
assert c.int_trait == 5
|
||||
assert c.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.') ]
|
||||
config = cl.load_config(argv)
|
||||
assert config.a == '10'
|
||||
assert config.b == '20'
|
||||
assert config.Foo.Bar.value == '10'
|
||||
# non-literal expressions are not evaluated
|
||||
self.assertEqual(config.Foo.Bam.value, 'list(range(10))')
|
||||
self.assertEqual(Unicode().from_string(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)
|
||||
u = Unicode()
|
||||
self.assertEqual(u.from_string(config.a), os.path.expanduser('~/1/2/3'))
|
||||
self.assertEqual(u.from_string(config.b), os.path.expanduser('~'))
|
||||
self.assertEqual(u.from_string(config.c), os.path.expanduser('~/'))
|
||||
self.assertEqual(u.from_string(config.d), '~/')
|
||||
|
||||
def test_extra_args(self):
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config(['--a=5', 'b', 'd', '--c=10'])
|
||||
self.assertEqual(cl.extra_args, ['b', 'd'])
|
||||
assert config.a == '5'
|
||||
assert config.c == '10'
|
||||
config = cl.load_config(['--', '--a=5', '--c=10'])
|
||||
self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
|
||||
|
||||
cl = self.klass(log=log)
|
||||
config = cl.load_config(['extra', '--a=2', '--c=1', '--', '-'])
|
||||
self.assertEqual(cl.extra_args, ['extra', '-'])
|
||||
|
||||
def test_unicode_args(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ['--a=épsîlön']
|
||||
config = cl.load_config(argv)
|
||||
print(config, cl.extra_args)
|
||||
self.assertEqual(config.a, 'épsîlön')
|
||||
|
||||
def test_list_append(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ["--C.list_trait", "x", "--C.list_trait", "y"]
|
||||
config = cl.load_config(argv)
|
||||
assert config.C.list_trait == ["x", "y"]
|
||||
c = C(config=config)
|
||||
assert c.list_trait == ["x", "y"]
|
||||
|
||||
def test_list_single_item(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ["--C.list_trait", "x"]
|
||||
config = cl.load_config(argv)
|
||||
c = C(config=config)
|
||||
assert c.list_trait == ["x"]
|
||||
|
||||
def test_dict(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ["--C.dict_trait", "x=5", "--C.dict_trait", "y=10"]
|
||||
config = cl.load_config(argv)
|
||||
c = C(config=config)
|
||||
assert c.dict_trait == {"x": "5", "y": "10"}
|
||||
|
||||
def test_dict_key_traits(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ["--C.dict_of_ints", "1=2", "--C.dict_of_ints", "3=4"]
|
||||
config = cl.load_config(argv)
|
||||
c = C(config=config)
|
||||
assert c.dict_of_ints == {1: 2, 3: 4}
|
||||
|
||||
|
||||
class CBase(Configurable):
|
||||
a = List().tag(config=True)
|
||||
b = List(Integer()).tag(config=True, multiplicity='*')
|
||||
c = List().tag(config=True, multiplicity='append')
|
||||
adict = Dict().tag(config=True)
|
||||
|
||||
|
||||
class CSub(CBase):
|
||||
d = Tuple().tag(config=True)
|
||||
e = Tuple().tag(config=True, multiplicity='+')
|
||||
bdict = Dict().tag(config=True, multiplicity='*')
|
||||
|
||||
|
||||
class TestArgParseKVCL(TestKeyValueCL):
|
||||
klass = KVArgParseConfigLoader
|
||||
|
||||
def test_no_cast_literals(self):
|
||||
cl = self.klass(log=log)
|
||||
# test ipython -c 1 doesn't cast to int
|
||||
argv = ["-c", "1"]
|
||||
config = cl.load_config(argv, aliases=dict(c="IPython.command_to_run"))
|
||||
assert config.IPython.command_to_run == "1"
|
||||
|
||||
def test_int_literals(self):
|
||||
cl = self.klass(log=log)
|
||||
# test ipython -c 1 doesn't cast to int
|
||||
argv = ["-c", "1"]
|
||||
config = cl.load_config(argv, aliases=dict(c="IPython.command_to_run"))
|
||||
assert config.IPython.command_to_run == "1"
|
||||
|
||||
def test_unicode_alias(self):
|
||||
cl = self.klass(log=log)
|
||||
argv = ['--a=épsîlön']
|
||||
config = cl.load_config(argv, aliases=dict(a='A.a'))
|
||||
print(dict(config))
|
||||
print(cl.extra_args)
|
||||
print(cl.aliases)
|
||||
self.assertEqual(config.A.a, 'épsîlön')
|
||||
|
||||
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'))
|
||||
|
||||
class A(Configurable):
|
||||
a = Unicode(config=True)
|
||||
b = Unicode(config=True)
|
||||
|
||||
a = A(config=config)
|
||||
self.assertEqual(a.a, os.path.expanduser('~/1/2/3'))
|
||||
self.assertEqual(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, "a=5")
|
||||
|
||||
def test_seq_traits(self):
|
||||
cl = self.klass(log=log, classes=(CBase, CSub))
|
||||
aliases = {'a3': 'CBase.c', 'a5': 'CSub.e'}
|
||||
argv = ("--CBase.a A --CBase.a 2 --CBase.b 1 --CBase.b 3 --a3 AA --CBase.c BB "
|
||||
"--CSub.d 1 --CSub.d BBB --CSub.e 1 --CSub.e=bcd a b c ").split()
|
||||
config = cl.load_config(argv, aliases=aliases)
|
||||
assert cl.extra_args == ["a", "b", "c"]
|
||||
assert config.CBase.a == ['A', '2']
|
||||
assert config.CBase.b == [1, 3]
|
||||
self.assertEqual(config.CBase.c, ['AA', 'BB'])
|
||||
|
||||
assert config.CSub.d == ('1', 'BBB')
|
||||
assert config.CSub.e == ('1', 'bcd')
|
||||
|
||||
def test_seq_traits_single_empty_string(self):
|
||||
cl = self.klass(log=log, classes=(CBase, ))
|
||||
aliases = {'seqopt': 'CBase.c'}
|
||||
argv = ['--seqopt', '']
|
||||
config = cl.load_config(argv, aliases=aliases)
|
||||
self.assertEqual(config.CBase.c, [''])
|
||||
|
||||
def test_dict_traits(self):
|
||||
cl = self.klass(log=log, classes=(CBase, CSub))
|
||||
aliases = {'D': 'CBase.adict', 'E': 'CSub.bdict'}
|
||||
argv = ["-D", "k1=v1", "-D=k2=2", "-D", "k3=v 3", "-E", "k=v", "-E", "22=222"]
|
||||
config = cl.load_config(argv, aliases=aliases)
|
||||
c = CSub(config=config)
|
||||
assert c.adict == {'k1': 'v1', 'k2': '2', 'k3': 'v 3'}
|
||||
assert c.bdict == {'k': 'v', '22': '222'}
|
||||
|
||||
def test_mixed_seq_positional(self):
|
||||
aliases = {"c": "Class.trait"}
|
||||
cl = self.klass(log=log, aliases=aliases)
|
||||
assignments = [("-c", "1"), ("--Class.trait=2",), ("--c=3",), ("--Class.trait", "4")]
|
||||
positionals = ["a", "b", "c"]
|
||||
# test with positionals at any index
|
||||
for idx in range(len(assignments) + 1):
|
||||
argv_parts = assignments[:]
|
||||
argv_parts[idx:idx] = (positionals,)
|
||||
argv = list(chain(*argv_parts))
|
||||
|
||||
config = cl.load_config(argv)
|
||||
assert config.Class.trait == ["1", "2", "3", "4"]
|
||||
assert cl.extra_args == ["a", "b", "c"]
|
||||
|
||||
def test_split_positional(self):
|
||||
"""Splitting positionals across flags is no longer allowed in traitlets 5"""
|
||||
cl = self.klass(log=log)
|
||||
argv = ["a", "--Class.trait=5", "b"]
|
||||
with pytest.raises(SystemExit):
|
||||
cl.load_config(argv)
|
||||
|
||||
|
||||
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_lazy_config_repr(self):
|
||||
cfg = Config()
|
||||
cfg.Class.lazy.append(1)
|
||||
cfg_repr = repr(cfg)
|
||||
assert '<LazyConfigValue' in cfg_repr
|
||||
assert "extend" in cfg_repr
|
||||
assert " [1]}>" in cfg_repr
|
||||
assert 'value=' not in cfg_repr
|
||||
cfg.Class.lazy.get_value([0])
|
||||
repr2 = repr(cfg)
|
||||
assert repr([0,1]) in repr2
|
||||
assert 'value=' in repr2
|
||||
|
||||
|
||||
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])
|
||||
|
||||
|
||||
def test_merge_multi_lazy(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
If systemwide overwirte and user append, we want both in the right
|
||||
order.
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait = [1]
|
||||
c2.Foo.trait.append(2)
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait, [1,2] )
|
||||
|
||||
|
||||
|
||||
def test_merge_multi_lazyII(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
If both are lazy we still want a lazy config.
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait.append(1)
|
||||
c2.Foo.trait.append(2)
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait._extend, [1,2] )
|
||||
|
||||
def test_merge_multi_lazy_III(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
Prepend should prepend in the right order.
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait = [1]
|
||||
c2.Foo.trait.prepend([0])
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait, [0, 1] )
|
||||
|
||||
def test_merge_multi_lazy_IV(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
Both prepending should be lazy
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait.prepend([1])
|
||||
c2.Foo.trait.prepend([0])
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait._prepend, [0, 1])
|
||||
|
||||
def test_merge_multi_lazy_update_I(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
dict update shoudl be in the right order.
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait = {"a": 1, "z": 26}
|
||||
c2.Foo.trait.update({"a": 0, "b": 1})
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait, {"a": 0, "b": 1, "z": 26})
|
||||
|
||||
def test_merge_multi_lazy_update_II(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
Later dict overwrite lazyness
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait.update({"a": 0, "b": 1})
|
||||
c2.Foo.trait = {"a": 1, "z": 26}
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait, {"a": 1, "z": 26})
|
||||
|
||||
def test_merge_multi_lazy_update_III(self):
|
||||
"""
|
||||
With multiple config files (systemwide and users), we want compounding.
|
||||
|
||||
Later dict overwrite lazyness
|
||||
"""
|
||||
c1 = Config()
|
||||
c2 = Config()
|
||||
|
||||
c1.Foo.trait.update({"a": 0, "b": 1})
|
||||
c2.Foo.trait.update({"a": 1, "z": 26})
|
||||
|
||||
c = Config()
|
||||
c.merge(c1)
|
||||
c.merge(c2)
|
||||
|
||||
self.assertEqual(c.Foo.trait._update, {"a": 1, "z": 26, "b": 1})
|
||||
Loading…
Add table
Add a link
Reference in a new issue