#!/usr/bin/env pythonw
#
# Copyright 2014-2017 European Commission (JRC);
# Licensed under the EUPL (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
#
"""Commands to inspect configurations and other cli infos."""
from collections import OrderedDict
from typing import Text, List
import os
import sys
from toolz import dicttoolz as dtz
import functools as fnt
import os.path as osp
from . import cmdlets
from .._vendor import traitlets as trt
from .._vendor.traitlets import config as trc
from .._vendor.traitlets.traitlets import Dict, Bool, FuzzyEnum, Instance, Unicode
def prepare_matcher(terms, is_regex):
import re
def matcher(r):
if is_regex:
return re.compile(r, re.I).search
else:
return lambda w: r.lower() in w.lower()
matchers = [matcher(t) for t in terms]
def match(word):
return any(m(word) for m in matchers)
return match
[docs]def prepare_search_map(all_classes, own_traits):
"""
:param own_traits:
bool or None (no traits)
:return:
``{'ClassName.trait_name': (class, trait)`` When `own_traits` not None,
``{clsname: class}``) otherwise.
Note: 1st case might contain None as trait!
"""
if own_traits is None:
return OrderedDict([
(cls.__name__, cls)
for cls in all_classes])
if own_traits:
class_traits = lambda cls: cls.class_own_traits(config=True)
else:
class_traits = lambda cls: cls.class_traits(config=True)
## Not using comprehension
# to work for classes with no traits.
#
smap = []
for cls in all_classes:
clsname = cls.__name__
traits = class_traits(cls)
if not traits:
smap.append((clsname + '.', (cls, None)))
continue
for attr, trait in sorted(traits.items()):
smap.append(('%s.%s' % (clsname, attr), (cls, trait)))
return OrderedDict(smap)
def prepare_help_selector(only_class_in_values, verbose):
if only_class_in_values:
if verbose:
def selector(ne, cls):
htext = cls.class_get_help()
if hasattr(cls, 'interpolations'):
htext = cls.interpolations.interp(htext, cls, _stub_keys=True)
return htext
else:
def selector(ne, cls):
help_lines = []
base_classes = ', '.join(p.__name__ for p in cls.__bases__)
help_lines.append(u'%s(%s)' % (cls.__name__, base_classes))
help_lines.append(len(help_lines[0]) * u'-')
help_lines.extend(cmdlets.class_help_description_lines(cls))
help_lines.append('')
try:
txt = cls.examples.default_value.strip()
if txt:
help_lines.append("Examples")
help_lines.append("--------")
help_lines.append(trc.indent(trc.dedent(txt)))
help_lines.append('')
except AttributeError:
pass
htext = '\n'.join(help_lines)
if hasattr(cls, 'interpolations'):
htext = cls.interpolations.interp(htext, _stub_keys=True)
return htext
else:
def selector(name, v):
cls, attr = v
if not attr:
#
## Not verbose and class not owning any trait.
return "--%s" % name
else:
return cls.class_get_trait_help(attr)
return selector
class _ConfigBase(cmdlets.Cmd):
#: Inheritance patching, below, side-effects subcommands from root-app.
#: NOTE that (almost) nothing else works than trait-defaults,
#: not class-property, neither set on constructor, nor on parent set.
#: (trait-default would also work)
subcommands = Dict({})
@trt.observe('parent')
def _rebase_hierarchy(self, change):
"""
Monkeypatch inheritance, so configurations reported as from main-app.
"""
parent = change.new
if parent:
rootapp = parent.root_object()
assert rootapp is not self, self # trait-setup error
root_class = type(rootapp)
## Conditions below were hard to come up with,
# to ensure no base-cycles and consistency.
#
if (issubclass(root_class, cmdlets.Cmd) and
not issubclass(root_class, _ConfigBase) and
root_class not in type(self).mro()):
_ConfigBase.__bases__ = (root_class, cmdlets.Cmd)
[docs]class ConfigCmd(_ConfigBase):
"Commands to manage configurations and other cli infos."
examples = Unicode("""
- Ask help on parameters affecting the source of the configurations::
{cmd_chain} desc config_paths show_config
- Show config-param values for all params containing word "mail"::
{cmd_chain} show --versbose mail
- Show values originating from files::
{cmd_chain} show --source file
- Show configuration paths::
{cmd_chain} paths
""")
## TODO: separate Spec from cmdlets, and ConfigCmd flags.
flags = {
('v', 'verbose'): (
{'Spec': {'verbose': True}},
cmdlets.Spec.verbose.help
),
('n', 'dry-run'): (
{'Spec': {'dry_run': True}},
cmdlets.Spec.dry_run.help
),
**cmdlets.Cmd.flags # @UndefinedVariable
}
aliases = {('f', 'force'): 'Spec.force'}
def _inherit_parent_cmd(self, change):
"""
Break cmdlet inheritance of main-cmd's flags, aliases and classes.
.. TIP::
This method has been decorated with :meth:`trt.observe` in cmdlets.
"""
pass
def __init__(self, **kwds):
super().__init__(
subcommands=cmdlets.build_sub_cmds(*config_subcmds),
**kwds)
[docs]class InfosCmd(_ConfigBase):
"""
List paths and other intallation infos.
Some of the environment-variables affecting configurations:
HOME, USERPROFILE, : where configs & DICE projects are stored
(1st one defined wins)
<APPNAME>_CONFIG_PATHS : where to read configuration-files.
"""
examples = Unicode("""
- Show parameter help for all classes/params containing 'foo' in their name::
{cmd_chain} foo
""")
app_infos = Instance(
dict,
default_value={},
help="Extra infos to put at the top of the output of this command"
)
def _collect_env_vars(self, classes):
classes = (cls
for cls
in self._classes_inc_parents(classes))
return [trait.metadata['envvar']
for cls in classes
for trait
in cls.class_own_traits(envvar=bool).values()]
[docs] def run(self, *args):
import inspect
if len(args) > 0:
raise cmdlets.CmdException(
"Cmd %r takes no arguments, received %d: %r!"
% (self.name, len(args), args))
sep = osp.sep
l2_yaml_list_sep = '\n - '
def format_tuple(path, files: List[Text]):
endpath = sep if path[-1] != sep else ''
return ' - %s%s: %s' % (path, endpath, files or '')
app_name = self.root_object().name
app_path = inspect.getfile(type(self.root_object()))
# TODO: paths not valid YAML! ...and renable TC.
yield "APP:"
app_infos_func = getattr(self, 'collect_app_infos', None)
if app_infos_func:
for kv in app_infos_func().items():
yield " %s: %s" % kv
yield " %s_path: %s" % (app_name, app_path)
yield " python_path: %s" % sys.prefix
for k, v in self.app_infos.items():
yield ' %s: %s' % (k, v)
yield "CONFIG:"
config_paths = l2_yaml_list_sep.join([''] + self.config_paths)
yield " config_paths: %s" % (config_paths or 'null')
loaded_cfgs = self.loaded_config_files
if loaded_cfgs:
yield " LOADED_CONFIGS:"
yield from (format_tuple(p, f) for p, f in loaded_cfgs)
else:
yield " LOADED_CONFIGS: null"
var_names = """HOME HOMEDRIVE HOMEPATH USERPROFILE
TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR"""
yield "ENV_VARS:"
trait_envvars = self._collect_env_vars(all_configurables(self))
for vname in sorted(set(var_names.split() + trait_envvars)):
yield " %s: %r" % (vname, os.environ.get(vname))
[docs]class ShowCmd(_ConfigBase):
"""
Print configurations (defaults | files | merged) before any validations.
SYNTAX
{cmd_chain} [OPTIONS] [--source=(merged | default)] [<search-term-1> ...]
{cmd_chain} [OPTIONS] --source file
- Search-terms are matched case-insensitively against '<class>.<param>'.
- Use --verbose to view values for config-params as they apply in the
whole hierarchy (not
- Results are sorted in "application order" (later configurations override
previous ones); use --sort for alphabetical order.
- Warning: Defaults/merged might not be always accurate!
- Tip: you may also add `--show-config` global option on any command
to view configured values accurately on runtime.
"""
examples = Unicode("""
- View all "merged" configuration values::
{cmd_chain}
- View all "default" or "in file" configuration values, respectively::
{cmd_chain} --source defaults
{cmd_chain} --s f
- View help on specific parameters::
{cmd_chain} config_paths
{cmd_chain} -e '.*path.*'
- List classes matching a regex::
{cmd_chain} -ecl '.*cmd$'
""")
verbose = Bool(
config=True,
help="Print infos from the whole hierarchy, including intermediate classes."
)
source = FuzzyEnum(
'defaults files merged'.split(),
default_value='merged',
allow_none=False,
help="""
Show configuration parameters in code, stored on disk files, or merged,
respectively."""
).tag(config=True)
list = Bool( # noqa: A003 (override-builtins)
help="Just list any matches."
).tag(config=True)
regex = Bool(
help="Search terms as regular-expressions."
).tag(config=True)
sort = Bool(
help="""
Sort classes alphabetically; by default, classes listed in "application order",
that is, later configurations override previous ones.
"""
).tag(config=True)
def __init__(self, **kwds):
kwds.setdefault('raise_config_file_errors', False)
self.aliases = {
('s', 'source'): ('ShowCmd.source',
ShowCmd.source.help)
}
self.flags = {
('l', 'list'): (
{type(self).__name__: {'list': True}},
type(self).list.help
),
('e', 'regex'): (
{type(self).__name__: {'regex': True}},
type(self).regex.help
),
('t', 'sort'): (
{type(self).__name__: {'sort': True}},
type(self).sort.help
),
}
super().__init__(**kwds)
[docs] @trc.catch_config_error
def initialize(self, argv=None):
"""Override to store file-configs separately (before merge)."""
super().initialize.__wrapped__(self, argv) # not to re-catch_config_error
cfg = self.read_config_files()
self._loaded_config = cfg
def _yield_file_configs(self, config, classes=None):
assert not classes, (classes, "should be empty")
for k, v in config.items():
yield k
try:
for kk, vv in v.items():
yield ' +--%s = %s' % (kk, vv)
except Exception as _:
yield ' +--%s' % v
def instanciate_class(self, cls, clsname, config):
try:
obj = cls(config=config)
except Exception as ex:
self.log.warning("Falied initializing class '%s' due to: %r",
clsname, ex)
## Assign config-values as dummy-object's attributes.
# Note: no merging of values now!
#
class C:
pass
obj = C()
obj.__dict__ = dict(config[clsname])
return obj
def _yield_configs_and_defaults(self, config, search_terms,
merged: bool):
verbose = self.verbose
conf_classes = all_configurables(self)
get_classes = (self._classes_inc_parents
if verbose else
self._classes_with_config_traits)
all_classes = list(get_classes(conf_classes))
## Merging needs to visit all hierarchy.
own_traits = not (verbose or merged)
search_map = prepare_search_map(all_classes, own_traits)
if search_terms:
matcher = prepare_matcher(search_terms, self.regex)
search_map = dtz.keyfilter(matcher, search_map)
items = search_map.items()
if self.sort:
items = sorted(items) # Sort by class-name (traits always sorted).
classes_configured = {}
for key, (cls, trait) in items:
if self.list:
yield key
continue
if not trait:
## Not --verbose and class not owning traits.
continue
clsname, trtname = key.split('.')
## Print own traits only, even when "merge" visits all.
#
sup = super(cls, cls)
if not verbose and getattr(sup, trtname, None) is trait:
continue
obj = classes_configured.get(cls)
if obj is None:
## Instantiate classes only once, to merge values.
classes_configured[cls] = self.instanciate_class(
cls, clsname, config)
## Print 1 class-line for all its traits.
#
base_classes = ', '.join(p.__name__ for p in cls.__bases__)
yield '%s(%s)' % (clsname, base_classes)
if merged:
try:
val = getattr(obj, trtname, '??')
except trt.TraitError as ex:
self.log.warning("Cannot merge '%s' due to: %r", trtname, ex)
val = "<invalid due to: %s>" % ex
else:
val = repr(trait.default())
yield ' +--%s = %s' % (trtname, val)
[docs] def run(self, *args):
source = self.source.lower()
self.log.info("Listing '%s' values for search-terms: %s...",
source, args)
if source == 'files':
if len(args) > 0:
raise cmdlets.CmdException(
"Cmd '%s --source files' takes no arguments, received %d: %r!"
% (self.name, len(args), args))
func = self._yield_file_configs
elif source == 'defaults':
func = fnt.partial(self._yield_configs_and_defaults, merged=False)
elif source == 'merged':
func = fnt.partial(self._yield_configs_and_defaults, merged=True)
else:
raise AssertionError('Impossible enum: %s' % source)
config = self._loaded_config
yield from func(config, args)
[docs]class DescCmd(_ConfigBase):
"""
List and print help for configurable classes and parameters.
SYNTAX
{cmd_chain} [-l] [-c] [-t] [-v] [<search-term> ...]
- If no search-terms provided, returns all.
- Search-terms are matched case-insensitively against '<class>.<param>',
or against '<class>' if --class.
- Use --verbose (-v) to view config-params from the whole hierarchy, that is,
including those from intermediate classes.
- Use --class (-c) to view just the help-text of classes.
- Results are sorted in "application order" (later configurations override
previous ones); use --sort for alphabetical order.
"""
examples = Unicode(r"""
- Just List::
{cmd_chain} --list # List configurable parameters.
{cmd_chain} -l --class # List configurable classes.
{cmd_chain} -l --verbose # List config params in all hierarchy.
- Exploit the fact that <class>.<param> are separated with a dot('.)::
{cmd_chain} -l Cmd. # List commands and their own params.
{cmd_chain} -lv Cmd. # List commands including inherited params.
{cmd_chain} -l ceiver. # List params of TStampReceiver spec class.
{cmd_chain} -l .user # List parameters starting with 'user' prefix.
- Use regular expressions (--regex)::
{cmd_chain} -le ^t.+cmd # List params for cmds starting with 't'.
{cmd_chain} -le date$ # List params ending with 'date'.
{cmd_chain} -le mail.*\. # Search 'mail' anywhere in class-names.
{cmd_chain} -le \..*mail # Search 'mail' anywhere in param-names.
- Do all of the above and remove -l, like this::
{cmd_chain} -c DescCmd # View help for this cmd without its parameters.
{cmd_chain} -t Spec. # View help sorted alphabetically
""")
list = Bool( # noqa: A003 (override-builtins)
help="Just list any matches."
).tag(config=True)
clazz = Bool(
help="Print class-help only; matching happens also on class-names."
).tag(config=True)
regex = Bool(
help="""
Search terms as regular-expressions.
Example:
{cmd_chain} -e ^DescCmd.regex
will print the help-text of this parameter (--regex, -e).
"""
).tag(config=True)
sort = Bool(
help="""
Sort classes alphabetically; by default, classes listed in "application order",
that is, later configurations override previous ones.
"""
).tag(config=True)
def __init__(self, **kwds):
self.flags = {
('l', 'list'): (
{type(self).__name__: {'list': True}},
type(self).list.help
),
('e', 'regex'): (
{type(self).__name__: {'regex': True}},
type(self).regex.help
),
('c', 'class'): (
{type(self).__name__: {'clazz': True}},
type(self).clazz.help
),
('t', 'sort'): (
{type(self).__name__: {'sort': True}},
type(self).sort.help
),
}
super().__init__(**kwds)
[docs] def run(self, *args):
## Prefer to modify `class_names` after `initialize()`, or else,
# the cmd options would be irrelevant and fatty :-)
get_classes = (self._classes_inc_parents
if self.clazz or self.verbose else
self._classes_with_config_traits)
all_classes = list(get_classes(all_configurables(self)))
own_traits = None if self.clazz else not self.verbose
search_map = prepare_search_map(all_classes, own_traits)
if args:
matcher = prepare_matcher(args, self.regex)
search_map = dtz.keyfilter(matcher, search_map)
items = search_map.items()
if self.sort:
items = sorted(items) # Sort by class-name (traits always sorted).
selector = prepare_help_selector(self.clazz, self.verbose)
for name, v in items:
if self.list:
yield name
else:
yield selector(name, v)
config_subcmds = (
InfosCmd,
ShowCmd,
DescCmd,
)
def all_configurables(cmd):
return [ConfigCmd] + list(config_subcmds) + cmd.all_app_configurables