application.py
582 lines
| 20.6 KiB
| text/x-python
|
PythonLexer
|
r3790 | # encoding: utf-8 | ||
""" | ||||
A base class for a configurable application. | ||||
Authors: | ||||
* Brian Granger | ||||
|
r4018 | * Min RK | ||
|
r3790 | """ | ||
#----------------------------------------------------------------------------- | ||||
# Copyright (C) 2008-2011 The IPython Development Team | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
|
r3794 | import logging | ||
|
r4025 | import os | ||
|
r3952 | import re | ||
|
r3790 | import sys | ||
|
r4025 | from copy import deepcopy | ||
|
r4977 | from collections import defaultdict | ||
|
r3790 | |||
|
r5172 | from IPython.external.decorator import decorator | ||
|
r3794 | from IPython.config.configurable import SingletonConfigurable | ||
|
r3790 | from IPython.config.loader import ( | ||
|
r4905 | KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, | ||
|
r3790 | ) | ||
|
r3852 | from IPython.utils.traitlets import ( | ||
|
r5344 | Unicode, List, Enum, Dict, Instance, TraitError | ||
|
r3852 | ) | ||
|
r3949 | from IPython.utils.importstring import import_item | ||
|
r4020 | from IPython.utils.text import indent, wrap_paragraphs, dedent | ||
#----------------------------------------------------------------------------- | ||||
# function for re-wrapping a helpstring | ||||
#----------------------------------------------------------------------------- | ||||
|
r3852 | |||
|
r3790 | #----------------------------------------------------------------------------- | ||
|
r3944 | # Descriptions for the various sections | ||
|
r3855 | #----------------------------------------------------------------------------- | ||
|
r4195 | # merge flags&aliases into options | ||
option_description = """ | ||||
|
r4254 | Arguments that take values are actually convenience aliases to full | ||
Configurables, whose aliases are listed on the help line. For more information | ||||
on full configurables, see '--help-all'. | ||||
|
r3855 | """.strip() # trim newlines of front and back | ||
keyvalue_description = """ | ||||
Parameters are set from command-line arguments of the form: | ||||
|
r4189 | `--Class.trait=value`. | ||
|
r4195 | This line is evaluated in Python, so simple expressions are allowed, e.g.:: | ||
`--C.a='range(3)'` For setting C.a=[0,1,2]. | ||||
|
r3855 | """.strip() # trim newlines of front and back | ||
|
r11340 | # 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 | ||||
|
r11103 | if not hasattr(sys, "argv"): | ||
sys.argv = [""] | ||||
|
r4195 | subcommand_description = """ | ||
Subcommands are launched as `{app} cmd [args]`. For information on using | ||||
subcommand 'cmd', do: `{app} cmd -h`. | ||||
""".strip().format(app=os.path.basename(sys.argv[0])) | ||||
# get running program name | ||||
|
r3855 | #----------------------------------------------------------------------------- | ||
|
r3790 | # Application class | ||
#----------------------------------------------------------------------------- | ||||
|
r5172 | @decorator | ||
|
r5214 | def catch_config_error(method, app, *args, **kwargs): | ||
|
r5172 | """Method decorator for catching invalid config (Trait/ArgumentErrors) during init. | ||
On a TraitError (generally caused by bad config), this will print the trait's | ||||
message, and exit the app. | ||||
For use on init methods, to prevent invoking excepthook on invalid input. | ||||
""" | ||||
try: | ||||
return method(app, *args, **kwargs) | ||||
except (TraitError, ArgumentError) as e: | ||||
app.print_help() | ||||
app.log.fatal("Bad config encountered during initialization:") | ||||
app.log.fatal(str(e)) | ||||
app.log.debug("Config at the time: %s", app.config) | ||||
app.exit(1) | ||||
|
r3790 | |||
|
r3940 | class ApplicationError(Exception): | ||
pass | ||||
|
r10559 | 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 | ||||
|
r10562 | highlevel_format = " %(levelname)s |" | ||
|
r10559 | |||
def format(self, record): | ||||
if record.levelno >= self.highlevel_limit: | ||||
record.highlevel = self.highlevel_format % record.__dict__ | ||||
else: | ||||
record.highlevel = "" | ||||
|
r12109 | if sys.version_info[:2] > (2,6): | ||
return super(LevelFormatter, self).format(record) | ||||
else: | ||||
return logging.Formatter.format(self, record) | ||||
|
r10559 | |||
|
r3940 | |||
|
r3794 | class Application(SingletonConfigurable): | ||
|
r3796 | """A singleton application with full configuration support.""" | ||
|
r3790 | |||
# The name of the application, will usually match the name of the command | ||||
# line application | ||||
|
r3942 | name = Unicode(u'application') | ||
|
r3790 | |||
# The description of the application that is printed at the beginning | ||||
# of the help. | ||||
description = Unicode(u'This is an application.') | ||||
|
r3855 | # default section descriptions | ||
|
r4195 | option_description = Unicode(option_description) | ||
|
r3855 | keyvalue_description = Unicode(keyvalue_description) | ||
|
r4195 | subcommand_description = Unicode(subcommand_description) | ||
|
r4215 | |||
# The usage and example string that goes at the end of the help string. | ||||
examples = Unicode() | ||||
|
r3790 | |||
# A sequence of Configurable subclasses whose config=True attributes will | ||||
|
r3861 | # be exposed at the command line. | ||
|
r3790 | classes = List([]) | ||
# The version string of this application. | ||||
version = Unicode(u'0.0') | ||||
|
r12342 | |||
# the argv used to initialize the application | ||||
|
r12716 | argv = List() | ||
|
r3790 | |||
|
r3795 | # The log level for the application | ||
|
r4025 | log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'), | ||
default_value=logging.WARN, | ||||
config=True, | ||||
help="Set the log level by value or name.") | ||||
def _log_level_changed(self, name, old, new): | ||||
|
r4039 | """Adjust the log level when log_level is set.""" | ||
|
r4025 | if isinstance(new, basestring): | ||
|
r4039 | new = getattr(logging, new) | ||
self.log_level = new | ||||
self.log.setLevel(new) | ||||
|
r6883 | |||
|
r10559 | log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True, | ||
help="The date format used by logging formatters for %(asctime)s" | ||||
) | ||||
def _log_datefmt_changed(self, name, old, new): | ||||
self._log_format_changed() | ||||
log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True, | ||||
|
r6884 | help="The Logging format template", | ||
) | ||||
|
r10475 | def _log_format_changed(self, name, old, new): | ||
"""Change the log formatter when log_format is set.""" | ||||
_log_handler = self.log.handlers[0] | ||||
|
r10989 | _log_formatter = LevelFormatter(new, datefmt=self.log_datefmt) | ||
|
r10475 | _log_handler.setFormatter(_log_formatter) | ||
|
r6883 | log = Instance(logging.Logger) | ||
def _log_default(self): | ||||
"""Start logging for this application. | ||||
|
r9547 | The default is to log to stderr using a StreamHandler, if no default | ||
|
r9546 | handler already exists. The log level starts at logging.WARN, but this | ||
can be adjusted by setting the ``log_level`` attribute. | ||||
|
r6883 | """ | ||
log = logging.getLogger(self.__class__.__name__) | ||||
log.setLevel(self.log_level) | ||||
|
r10203 | log.propagate = False | ||
|
r9546 | _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 | ||||
|
r6883 | if sys.executable.endswith('pythonw.exe'): | ||
# this should really go to a file, but file-logging is only | ||||
# hooked up in parallel applications | ||||
_log_handler = logging.StreamHandler(open(os.devnull, 'w')) | ||||
else: | ||||
_log_handler = logging.StreamHandler() | ||||
|
r10559 | _log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt) | ||
|
r6883 | _log_handler.setFormatter(_log_formatter) | ||
log.addHandler(_log_handler) | ||||
return log | ||||
|
r4872 | |||
|
r3861 | # the alias map for configurables | ||
|
r4214 | aliases = Dict({'log-level' : 'Application.log_level'}) | ||
|
r4872 | |||
|
r3861 | # flags for loading Configurables or store_const style flags | ||
# flags are loaded from this dict by '--key' flags | ||||
# this must be a dict of two-tuples, the first element being the Config/dict | ||||
# and the second being the help string for the flag | ||||
flags = Dict() | ||||
|
r4020 | def _flags_changed(self, name, old, new): | ||
"""ensure flags dict is valid""" | ||||
for key,value in new.iteritems(): | ||||
assert len(value) == 2, "Bad flag: %r:%s"%(key,value) | ||||
assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value) | ||||
assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value) | ||||
|
r4872 | |||
|
r3949 | # subcommands for launching other applications | ||
# if this is not empty, this will be a parent Application | ||||
|
r4872 | # this must be a dict of two-tuples, | ||
|
r4020 | # the first element being the application class/import string | ||
|
r3949 | # and the second being the help string for the subcommand | ||
subcommands = Dict() | ||||
# parse_command_line will initialize a subapp, if requested | ||||
subapp = Instance('IPython.config.application.Application', allow_none=True) | ||||
|
r4872 | |||
|
r3958 | # extra command-line arguments that don't set config values | ||
extra_args = List(Unicode) | ||||
|
r4872 | |||
|
r3794 | |||
|
r3790 | def __init__(self, **kwargs): | ||
|
r3794 | SingletonConfigurable.__init__(self, **kwargs) | ||
|
r4462 | # Ensure my class is in self.classes, so my attributes appear in command line | ||
# options and config files. | ||||
if self.__class__ not in self.classes: | ||||
self.classes.insert(0, self.__class__) | ||||
|
r4872 | |||
|
r3940 | def _config_changed(self, name, old, new): | ||
SingletonConfigurable._config_changed(self, name, old, new) | ||||
self.log.debug('Config changed:') | ||||
self.log.debug(repr(new)) | ||||
|
r5214 | @catch_config_error | ||
|
r3949 | def initialize(self, argv=None): | ||
"""Do the basic steps to configure me. | ||||
|
r4872 | |||
|
r3949 | Override in subclasses. | ||
""" | ||||
self.parse_command_line(argv) | ||||
|
r4872 | |||
|
r3949 | def start(self): | ||
"""Start the app mainloop. | ||||
|
r4872 | |||
|
r3949 | Override in subclasses. | ||
""" | ||||
if self.subapp is not None: | ||||
return self.subapp.start() | ||||
|
r4872 | |||
|
r3861 | def print_alias_help(self): | ||
|
r4020 | """Print the alias part of the help.""" | ||
|
r3861 | if not self.aliases: | ||
|
r3852 | return | ||
|
r4872 | |||
|
r4195 | lines = [] | ||
|
r3852 | classdict = {} | ||
|
r3951 | for cls in self.classes: | ||
|
r3952 | # include all parents (up to, but excluding Configurable) in available names | ||
for c in cls.mro()[:-3]: | ||||
|
r3951 | classdict[c.__name__] = c | ||
|
r4872 | |||
|
r3861 | for alias, longname in self.aliases.iteritems(): | ||
|
r3852 | classname, traitname = longname.split('.',1) | ||
cls = classdict[classname] | ||||
|
r4872 | |||
|
r3852 | trait = cls.class_traits(config=True)[traitname] | ||
|
r4189 | help = cls.class_get_trait_help(trait).splitlines() | ||
# reformat first line | ||||
help[0] = help[0].replace(longname, alias) + ' (%s)'%longname | ||||
|
r4605 | if len(alias) == 1: | ||
help[0] = help[0].replace('--%s='%alias, '-%s '%alias) | ||||
|
r4189 | lines.extend(help) | ||
|
r4195 | # lines.append('') | ||
|
r4189 | print os.linesep.join(lines) | ||
|
r4872 | |||
|
r3861 | def print_flag_help(self): | ||
|
r4020 | """Print the flag part of the help.""" | ||
|
r3861 | if not self.flags: | ||
|
r3852 | return | ||
|
r4872 | |||
|
r4195 | lines = [] | ||
|
r3861 | for m, (cfg,help) in self.flags.iteritems(): | ||
|
r4605 | prefix = '--' if len(m) > 1 else '-' | ||
lines.append(prefix+m) | ||||
|
r4020 | lines.append(indent(dedent(help.strip()))) | ||
|
r4195 | # lines.append('') | ||
print os.linesep.join(lines) | ||||
|
r4872 | |||
|
r4195 | def print_options(self): | ||
if not self.flags and not self.aliases: | ||||
return | ||||
lines = ['Options'] | ||||
lines.append('-'*len(lines[0])) | ||||
|
r3944 | lines.append('') | ||
|
r4195 | for p in wrap_paragraphs(self.option_description): | ||
lines.append(p) | ||||
lines.append('') | ||||
print os.linesep.join(lines) | ||||
self.print_flag_help() | ||||
self.print_alias_help() | ||||
|
r4872 | |||
|
r3949 | def print_subcommands(self): | ||
|
r4020 | """Print the subcommand part of the help.""" | ||
|
r3949 | if not self.subcommands: | ||
return | ||||
|
r4872 | |||
|
r3949 | lines = ["Subcommands"] | ||
lines.append('-'*len(lines[0])) | ||||
|
r4195 | lines.append('') | ||
for p in wrap_paragraphs(self.subcommand_description): | ||||
lines.append(p) | ||||
lines.append('') | ||||
|
r4254 | for subc, (cls, help) in self.subcommands.iteritems(): | ||
lines.append(subc) | ||||
|
r3949 | if help: | ||
|
r4020 | lines.append(indent(dedent(help.strip()))) | ||
|
r3949 | lines.append('') | ||
|
r4195 | print os.linesep.join(lines) | ||
|
r4872 | |||
|
r3946 | def print_help(self, classes=False): | ||
"""Print the help for each Configurable class in self.classes. | ||||
|
r4872 | |||
|
r4020 | If classes=False (the default), only flags and aliases are printed. | ||
|
r3946 | """ | ||
|
r11664 | self.print_description() | ||
|
r3952 | self.print_subcommands() | ||
|
r4195 | self.print_options() | ||
|
r4872 | |||
|
r3946 | if classes: | ||
if self.classes: | ||||
print "Class parameters" | ||||
print "----------------" | ||||
|
r4020 | for p in wrap_paragraphs(self.keyvalue_description): | ||
print p | ||||
|
r4872 | |||
|
r3946 | for cls in self.classes: | ||
cls.class_print_help() | ||||
else: | ||||
print "To see all available configurables, use `--help-all`" | ||||
|
r3790 | |||
|
r11664 | self.print_examples() | ||
|
r3790 | def print_description(self): | ||
"""Print the application description.""" | ||||
|
r4020 | for p in wrap_paragraphs(self.description): | ||
print p | ||||
|
r3790 | |||
|
r4215 | def print_examples(self): | ||
"""Print usage and examples. | ||||
This usage string goes at the end of the command line help string | ||||
and should contain examples of the application's usage. | ||||
""" | ||||
if self.examples: | ||||
print "Examples" | ||||
print "--------" | ||||
print indent(dedent(self.examples.strip())) | ||||
|
r3790 | def print_version(self): | ||
"""Print the version string.""" | ||||
print self.version | ||||
def update_config(self, config): | ||||
|
r3796 | """Fire the traits events when the config is updated.""" | ||
|
r3790 | # Save a copy of the current config. | ||
newconfig = deepcopy(self.config) | ||||
# Merge the new config into the current one. | ||||
|
r10873 | newconfig.merge(config) | ||
|
r3790 | # Save the combined config as self.config, which triggers the traits | ||
# events. | ||||
|
r3941 | self.config = newconfig | ||
|
r4872 | |||
|
r5214 | @catch_config_error | ||
|
r3949 | def initialize_subcommand(self, subc, argv=None): | ||
|
r4020 | """Initialize a subcommand with argv.""" | ||
|
r3958 | subapp,help = self.subcommands.get(subc) | ||
|
r4872 | |||
|
r3949 | if isinstance(subapp, basestring): | ||
subapp = import_item(subapp) | ||||
|
r4872 | |||
|
r3962 | # clear existing instances | ||
self.__class__.clear_instance() | ||||
|
r3949 | # instantiate | ||
|
r11006 | self.subapp = subapp.instance(config=self.config) | ||
|
r3949 | # and initialize subapp | ||
self.subapp.initialize(argv) | ||||
|
r4977 | |||
def flatten_flags(self): | ||||
"""flatten flags and aliases, so cl-args override as expected. | ||||
This prevents issues such as an alias pointing to InteractiveShell, | ||||
but a config file setting the same trait in TerminalInteraciveShell | ||||
getting inappropriate priority over the command-line arg. | ||||
Only aliases with exactly one descendent in the class list | ||||
will be promoted. | ||||
""" | ||||
# build a tree of classes in our list that inherit from a particular | ||||
# it will be a dict by parent classname of classes in our list | ||||
# that are descendents | ||||
mro_tree = defaultdict(list) | ||||
for cls in self.classes: | ||||
clsname = cls.__name__ | ||||
for parent in cls.mro()[1:-3]: | ||||
# exclude cls itself and Configurable,HasTraits,object | ||||
mro_tree[parent.__name__].append(clsname) | ||||
# flatten aliases, which have the form: | ||||
# { 'alias' : 'Class.trait' } | ||||
aliases = {} | ||||
for alias, cls_trait in self.aliases.iteritems(): | ||||
cls,trait = cls_trait.split('.',1) | ||||
children = mro_tree[cls] | ||||
if len(children) == 1: | ||||
# exactly one descendent, promote alias | ||||
cls = children[0] | ||||
aliases[alias] = '.'.join([cls,trait]) | ||||
# flatten flags, which are of the form: | ||||
# { 'key' : ({'Cls' : {'trait' : value}}, 'help')} | ||||
flags = {} | ||||
for key, (flagdict, help) in self.flags.iteritems(): | ||||
newflag = {} | ||||
for cls, subdict in flagdict.iteritems(): | ||||
children = mro_tree[cls] | ||||
# exactly one descendent, promote flag section | ||||
if len(children) == 1: | ||||
cls = children[0] | ||||
newflag[cls] = subdict | ||||
flags[key] = (newflag, help) | ||||
return flags, aliases | ||||
|
r4872 | |||
|
r5214 | @catch_config_error | ||
|
r3790 | def parse_command_line(self, argv=None): | ||
"""Parse the command line arguments.""" | ||||
|
r3795 | argv = sys.argv[1:] if argv is None else argv | ||
|
r12342 | self.argv = list(argv) | ||
|
r6166 | |||
if argv and argv[0] == 'help': | ||||
# turn `ipython help notebook` into `ipython notebook -h` | ||||
argv = argv[1:] + ['-h'] | ||||
|
r3790 | |||
|
r3952 | if self.subcommands and len(argv) > 0: | ||
# we have subcommands, and one may have been specified | ||||
subc, subargv = argv[0], argv[1:] | ||||
|
r3958 | if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands: | ||
|
r3952 | # it's a subcommand, and *not* a flag or class parameter | ||
return self.initialize_subcommand(subc, subargv) | ||||
|
r4872 | |||
|
r8842 | # 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) | ||||
|
r3945 | self.exit(0) | ||
|
r3790 | |||
|
r8842 | if '--version' in interpreted_argv or '-V' in interpreted_argv: | ||
|
r3790 | self.print_version() | ||
|
r3945 | self.exit(0) | ||
|
r4977 | |||
# flatten flags&aliases, so cl-args get appropriate priority: | ||||
flags,aliases = self.flatten_flags() | ||||
|
r4872 | |||
|
r4977 | loader = KVArgParseConfigLoader(argv=argv, aliases=aliases, | ||
flags=flags) | ||||
|
r5172 | config = loader.load_config() | ||
self.update_config(config) | ||||
|
r3958 | # store unparsed args in extra_args | ||
self.extra_args = loader.extra_args | ||||
|
r3790 | |||
|
r5214 | @catch_config_error | ||
|
r3790 | def load_config_file(self, filename, path=None): | ||
"""Load a .py based config file by filename and path.""" | ||||
loader = PyFileConfigLoader(filename, path=path) | ||||
|
r4461 | try: | ||
config = loader.load_config() | ||||
|
r4905 | except ConfigFileNotFound: | ||
# problem finding the file, raise | ||||
|
r4465 | raise | ||
|
r4461 | except Exception: | ||
|
r4564 | # try to get the full filename, but it will be empty in the | ||
# unlikely event that the error raised before filefind finished | ||||
filename = loader.full_filename or filename | ||||
|
r4465 | # problem while running the file | ||
|
r4564 | self.log.error("Exception while loading config file %s", | ||
filename, exc_info=True) | ||||
|
r4461 | else: | ||
|
r4564 | self.log.debug("Loaded config file: %s", loader.full_filename) | ||
|
r4461 | self.update_config(config) | ||
|
r4872 | |||
|
r4025 | def generate_config_file(self): | ||
"""generate default config file from Configurables""" | ||||
lines = ["# Configuration file for %s."%self.name] | ||||
lines.append('') | ||||
lines.append('c = get_config()') | ||||
lines.append('') | ||||
for cls in self.classes: | ||||
lines.append(cls.class_config_section()) | ||||
return '\n'.join(lines) | ||||
|
r3790 | |||
|
r3940 | def exit(self, exit_status=0): | ||
self.log.debug("Exiting application: %s" % self.name) | ||||
sys.exit(exit_status) | ||||
|
r3944 | |||
|
r11171 | @classmethod | ||
|
r11176 | 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 | ||||
""" | ||||
|
r11171 | app = cls.instance(**kwargs) | ||
app.initialize(argv) | ||||
app.start() | ||||
|
r3944 | #----------------------------------------------------------------------------- | ||
# utility functions, for convenience | ||||
#----------------------------------------------------------------------------- | ||||
def boolean_flag(name, configurable, set_help='', unset_help=''): | ||||
|
r4020 | """Helper for building basic --trait, --no-trait flags. | ||
|
r4872 | |||
|
r3944 | Parameters | ||
---------- | ||||
|
r4872 | |||
|
r3944 | 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 | ||||
|
r4872 | |||
|
r3944 | Returns | ||
------- | ||||
|
r4872 | |||
|
r3944 | 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 | ||||
|
r4872 | |||
|
r3944 | cls,trait = configurable.split('.') | ||
|
r4872 | |||
|
r3957 | setter = {cls : {trait : True}} | ||
unsetter = {cls : {trait : False}} | ||||
|
r3944 | return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)} | ||
|
r4020 | |||