Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/traitlets/config/application.py: 27%
496 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-01 06:54 +0000
1"""A base class for a configurable application."""
3# Copyright (c) IPython Development Team.
4# Distributed under the terms of the Modified BSD License.
7import functools
8import json
9import logging
10import os
11import pprint
12import re
13import sys
14import typing as t
15from collections import OrderedDict, defaultdict
16from contextlib import suppress
17from copy import deepcopy
18from logging.config import dictConfig
19from textwrap import dedent
20from typing import Any, Callable, TypeVar, cast
22from traitlets.config.configurable import Configurable, SingletonConfigurable
23from traitlets.config.loader import (
24 ArgumentError,
25 Config,
26 ConfigFileNotFound,
27 JSONFileConfigLoader,
28 KVArgParseConfigLoader,
29 PyFileConfigLoader,
30)
31from traitlets.traitlets import (
32 Bool,
33 Dict,
34 Enum,
35 Instance,
36 List,
37 TraitError,
38 Unicode,
39 default,
40 observe,
41 observe_compat,
42)
43from traitlets.utils.nested_update import nested_update
44from traitlets.utils.text import indent, wrap_paragraphs
46from ..utils import cast_unicode
47from ..utils.importstring import import_item
49# -----------------------------------------------------------------------------
50# Descriptions for the various sections
51# -----------------------------------------------------------------------------
52# merge flags&aliases into options
53option_description = """
54The options below are convenience aliases to configurable class-options,
55as listed in the "Equivalent to" description-line of the aliases.
56To see all configurable class-options for some <cmd>, use:
57 <cmd> --help-all
58""".strip() # trim newlines of front and back
60keyvalue_description = """
61The command-line option below sets the respective configurable class-parameter:
62 --Class.parameter=value
63This line is evaluated in Python, so simple expressions are allowed.
64For instance, to set `C.a=[0,1,2]`, you may type this:
65 --C.a='range(3)'
66""".strip() # trim newlines of front and back
68# sys.argv can be missing, for example when python is embedded. See the docs
69# for details: http://docs.python.org/2/c-api/intro.html#embedding-python
70if not hasattr(sys, "argv"):
71 sys.argv = [""]
73subcommand_description = """
74Subcommands are launched as `{app} cmd [args]`. For information on using
75subcommand 'cmd', do: `{app} cmd -h`.
76"""
77# get running program name
79# -----------------------------------------------------------------------------
80# Application class
81# -----------------------------------------------------------------------------
84_envvar = os.environ.get("TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR", "")
85if _envvar.lower() in {"1", "true"}:
86 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = True
87elif _envvar.lower() in {"0", "false", ""}:
88 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = False
89else:
90 raise ValueError(
91 "Unsupported value for environment variable: 'TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}."
92 % _envvar
93 )
96IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe")
98T = TypeVar("T", bound=Callable[..., Any])
101def catch_config_error(method: T) -> T:
102 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
104 On a TraitError (generally caused by bad config), this will print the trait's
105 message, and exit the app.
107 For use on init methods, to prevent invoking excepthook on invalid input.
108 """
110 @functools.wraps(method)
111 def inner(app, *args, **kwargs):
112 try:
113 return method(app, *args, **kwargs)
114 except (TraitError, ArgumentError) as e:
115 app.log.fatal("Bad config encountered during initialization: %s", e)
116 app.log.debug("Config at the time: %s", app.config)
117 app.exit(1)
119 return cast(T, inner)
122class ApplicationError(Exception):
123 pass
126class LevelFormatter(logging.Formatter):
127 """Formatter with additional `highlevel` record
129 This field is empty if log level is less than highlevel_limit,
130 otherwise it is formatted with self.highlevel_format.
132 Useful for adding 'WARNING' to warning messages,
133 without adding 'INFO' to info, etc.
134 """
136 highlevel_limit = logging.WARN
137 highlevel_format = " %(levelname)s |"
139 def format(self, record):
140 if record.levelno >= self.highlevel_limit:
141 record.highlevel = self.highlevel_format % record.__dict__
142 else:
143 record.highlevel = ""
144 return super().format(record)
147class Application(SingletonConfigurable):
148 """A singleton application with full configuration support."""
150 # The name of the application, will usually match the name of the command
151 # line application
152 name: t.Union[str, Unicode] = Unicode("application")
154 # The description of the application that is printed at the beginning
155 # of the help.
156 description: t.Union[str, Unicode] = Unicode("This is an application.")
157 # default section descriptions
158 option_description: t.Union[str, Unicode] = Unicode(option_description)
159 keyvalue_description: t.Union[str, Unicode] = Unicode(keyvalue_description)
160 subcommand_description: t.Union[str, Unicode] = Unicode(subcommand_description)
162 python_config_loader_class = PyFileConfigLoader
163 json_config_loader_class = JSONFileConfigLoader
165 # The usage and example string that goes at the end of the help string.
166 examples: t.Union[str, Unicode] = Unicode()
168 # A sequence of Configurable subclasses whose config=True attributes will
169 # be exposed at the command line.
170 classes: t.List[t.Type[t.Any]] = []
172 def _classes_inc_parents(self, classes=None):
173 """Iterate through configurable classes, including configurable parents
175 :param classes:
176 The list of classes to iterate; if not set, uses :attr:`classes`.
178 Children should always be after parents, and each class should only be
179 yielded once.
180 """
181 if classes is None:
182 classes = self.classes
184 seen = set()
185 for c in classes:
186 # We want to sort parents before children, so we reverse the MRO
187 for parent in reversed(c.mro()):
188 if issubclass(parent, Configurable) and (parent not in seen):
189 seen.add(parent)
190 yield parent
192 # The version string of this application.
193 version: t.Union[str, Unicode] = Unicode("0.0")
195 # the argv used to initialize the application
196 argv: t.Union[t.List[str], List] = List()
198 # Whether failing to load config files should prevent startup
199 raise_config_file_errors: t.Union[bool, Bool] = Bool(
200 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR
201 )
203 # The log level for the application
204 log_level: t.Union[str, int, Enum] = Enum(
205 (0, 10, 20, 30, 40, 50, "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"),
206 default_value=logging.WARN,
207 help="Set the log level by value or name.",
208 ).tag(config=True)
210 _log_formatter_cls = LevelFormatter
212 log_datefmt: t.Union[str, Unicode] = Unicode(
213 "%Y-%m-%d %H:%M:%S", help="The date format used by logging formatters for %(asctime)s"
214 ).tag(config=True)
216 log_format: t.Union[str, Unicode] = Unicode(
217 "[%(name)s]%(highlevel)s %(message)s",
218 help="The Logging format template",
219 ).tag(config=True)
221 def get_default_logging_config(self):
222 """Return the base logging configuration.
224 The default is to log to stderr using a StreamHandler, if no default
225 handler already exists.
227 The log handler level starts at logging.WARN, but this can be adjusted
228 by setting the ``log_level`` attribute.
230 The ``logging_config`` trait is merged into this allowing for finer
231 control of logging.
233 """
234 config: t.Dict[str, t.Any] = {
235 "version": 1,
236 "handlers": {
237 "console": {
238 "class": "logging.StreamHandler",
239 "formatter": "console",
240 "level": logging.getLevelName(self.log_level),
241 "stream": "ext://sys.stderr",
242 },
243 },
244 "formatters": {
245 "console": {
246 "class": (
247 f"{self._log_formatter_cls.__module__}"
248 f".{self._log_formatter_cls.__name__}"
249 ),
250 "format": self.log_format,
251 "datefmt": self.log_datefmt,
252 },
253 },
254 "loggers": {
255 self.__class__.__name__: {
256 "level": "DEBUG",
257 "handlers": ["console"],
258 }
259 },
260 "disable_existing_loggers": False,
261 }
263 if IS_PYTHONW:
264 # disable logging
265 # (this should really go to a file, but file-logging is only
266 # hooked up in parallel applications)
267 del config["handlers"]
268 del config["loggers"]
270 return config
272 @observe("log_datefmt", "log_format", "log_level", "logging_config")
273 def _observe_logging_change(self, change):
274 # convert log level strings to ints
275 log_level = self.log_level
276 if isinstance(log_level, str):
277 self.log_level = getattr(logging, log_level)
278 self._configure_logging()
280 @observe("log", type="default")
281 def _observe_logging_default(self, change):
282 self._configure_logging()
284 def _configure_logging(self):
285 config = self.get_default_logging_config()
286 nested_update(config, self.logging_config or {})
287 dictConfig(config)
288 # make a note that we have configured logging
289 self._logging_configured = True
291 @default("log")
292 def _log_default(self):
293 """Start logging for this application."""
294 log = logging.getLogger(self.__class__.__name__)
295 log.propagate = False
296 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
297 while _log:
298 if _log.handlers:
299 return log
300 if not _log.propagate:
301 break
302 else:
303 _log = _log.parent # type:ignore[assignment]
304 return log
306 logging_config = Dict(
307 help="""
308 Configure additional log handlers.
310 The default stderr logs handler is configured by the
311 log_level, log_datefmt and log_format settings.
313 This configuration can be used to configure additional handlers
314 (e.g. to output the log to a file) or for finer control over the
315 default handlers.
317 If provided this should be a logging configuration dictionary, for
318 more information see:
319 https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
321 This dictionary is merged with the base logging configuration which
322 defines the following:
324 * A logging formatter intended for interactive use called
325 ``console``.
326 * A logging handler that writes to stderr called
327 ``console`` which uses the formatter ``console``.
328 * A logger with the name of this application set to ``DEBUG``
329 level.
331 This example adds a new handler that writes to a file:
333 .. code-block:: python
335 c.Application.logging_config = {
336 'handlers': {
337 'file': {
338 'class': 'logging.FileHandler',
339 'level': 'DEBUG',
340 'filename': '<path/to/file>',
341 }
342 },
343 'loggers': {
344 '<application-name>': {
345 'level': 'DEBUG',
346 # NOTE: if you don't list the default "console"
347 # handler here then it will be disabled
348 'handlers': ['console', 'file'],
349 },
350 }
351 }
353 """,
354 ).tag(config=True)
356 #: the alias map for configurables
357 #: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`.
358 #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text),
359 # or just the "Class.trait" string, in which case the help text is inferred from the
360 # corresponding trait
361 aliases: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Union[str, t.Tuple[str, str]]] = {
362 "log-level": "Application.log_level"
363 }
365 # flags for loading Configurables or store_const style flags
366 # flags are loaded from this dict by '--key' flags
367 # this must be a dict of two-tuples, the first element being the Config/dict
368 # and the second being the help string for the flag
369 flags: t.Dict[
370 t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict[str, t.Any], Config], str]
371 ] = {
372 "debug": (
373 {
374 "Application": {
375 "log_level": logging.DEBUG,
376 },
377 },
378 "Set log-level to debug, for the most verbose logging.",
379 ),
380 "show-config": (
381 {
382 "Application": {
383 "show_config": True,
384 },
385 },
386 "Show the application's configuration (human-readable format)",
387 ),
388 "show-config-json": (
389 {
390 "Application": {
391 "show_config_json": True,
392 },
393 },
394 "Show the application's configuration (json format)",
395 ),
396 }
398 # subcommands for launching other applications
399 # if this is not empty, this will be a parent Application
400 # this must be a dict of two-tuples,
401 # the first element being the application class/import string
402 # and the second being the help string for the subcommand
403 subcommands: t.Union[t.Dict[str, t.Tuple[t.Any, str]], Dict] = Dict()
404 # parse_command_line will initialize a subapp, if requested
405 subapp = Instance("traitlets.config.application.Application", allow_none=True)
407 # extra command-line arguments that don't set config values
408 extra_args: t.Union[t.List[str], List] = List(Unicode())
410 cli_config = Instance(
411 Config,
412 (),
413 {},
414 help="""The subset of our configuration that came from the command-line
416 We re-load this configuration after loading config files,
417 to ensure that it maintains highest priority.
418 """,
419 )
421 _loaded_config_files = List()
423 show_config: t.Union[bool, Bool] = Bool(
424 help="Instead of starting the Application, dump configuration to stdout"
425 ).tag(config=True)
427 show_config_json: t.Union[bool, Bool] = Bool(
428 help="Instead of starting the Application, dump configuration to stdout (as JSON)"
429 ).tag(config=True)
431 @observe("show_config_json")
432 def _show_config_json_changed(self, change):
433 self.show_config = change.new
435 @observe("show_config")
436 def _show_config_changed(self, change):
437 if change.new:
438 self._save_start = self.start
439 self.start = self.start_show_config # type:ignore[assignment]
441 def __init__(self, **kwargs):
442 SingletonConfigurable.__init__(self, **kwargs)
443 # Ensure my class is in self.classes, so my attributes appear in command line
444 # options and config files.
445 cls = self.__class__
446 if cls not in self.classes:
447 if self.classes is cls.classes:
448 # class attr, assign instead of insert
449 self.classes = [cls] + self.classes
450 else:
451 self.classes.insert(0, self.__class__)
453 @observe("config")
454 @observe_compat
455 def _config_changed(self, change):
456 super()._config_changed(change)
457 self.log.debug("Config changed: %r", change.new)
459 @catch_config_error
460 def initialize(self, argv=None):
461 """Do the basic steps to configure me.
463 Override in subclasses.
464 """
465 self.parse_command_line(argv)
467 def start(self):
468 """Start the app mainloop.
470 Override in subclasses.
471 """
472 if self.subapp is not None:
473 return self.subapp.start()
475 def start_show_config(self):
476 """start function used when show_config is True"""
477 config = self.config.copy()
478 # exclude show_config flags from displayed config
479 for cls in self.__class__.mro():
480 if cls.__name__ in config:
481 cls_config = config[cls.__name__]
482 cls_config.pop("show_config", None)
483 cls_config.pop("show_config_json", None)
485 if self.show_config_json:
486 json.dump(config, sys.stdout, indent=1, sort_keys=True, default=repr)
487 # add trailing newline
488 sys.stdout.write("\n")
489 return
491 if self._loaded_config_files:
492 print("Loaded config files:")
493 for f in self._loaded_config_files:
494 print(" " + f)
495 print()
497 for classname in sorted(config):
498 class_config = config[classname]
499 if not class_config:
500 continue
501 print(classname)
502 pformat_kwargs: t.Dict[str, t.Any] = dict(indent=4, compact=True)
504 for traitname in sorted(class_config):
505 value = class_config[traitname]
506 print(
507 " .{} = {}".format(
508 traitname,
509 pprint.pformat(value, **pformat_kwargs),
510 )
511 )
513 def print_alias_help(self):
514 """Print the alias parts of the help."""
515 print("\n".join(self.emit_alias_help()))
517 def emit_alias_help(self):
518 """Yield the lines for alias part of the help."""
519 if not self.aliases:
520 return
522 classdict = {}
523 for cls in self.classes:
524 # include all parents (up to, but excluding Configurable) in available names
525 for c in cls.mro()[:-3]:
526 classdict[c.__name__] = c
528 fhelp: t.Optional[str]
529 for alias, longname in self.aliases.items():
530 try:
531 if isinstance(longname, tuple):
532 longname, fhelp = longname
533 else:
534 fhelp = None
535 classname, traitname = longname.split(".")[-2:]
536 longname = classname + "." + traitname
537 cls = classdict[classname]
539 trait = cls.class_traits(config=True)[traitname]
540 fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines()
542 if not isinstance(alias, tuple):
543 alias = (alias,)
544 alias = sorted(alias, key=len) # type:ignore[assignment]
545 alias = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in alias)
547 # reformat first line
548 assert fhelp is not None
549 fhelp[0] = fhelp[0].replace("--" + longname, alias) # type:ignore
550 yield from fhelp
551 yield indent("Equivalent to: [--%s]" % longname)
552 except Exception as ex:
553 self.log.error("Failed collecting help-message for alias %r, due to: %s", alias, ex)
554 raise
556 def print_flag_help(self):
557 """Print the flag part of the help."""
558 print("\n".join(self.emit_flag_help()))
560 def emit_flag_help(self):
561 """Yield the lines for the flag part of the help."""
562 if not self.flags:
563 return
565 for flags, (cfg, fhelp) in self.flags.items():
566 try:
567 if not isinstance(flags, tuple):
568 flags = (flags,)
569 flags = sorted(flags, key=len) # type:ignore[assignment]
570 flags = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in flags)
571 yield flags
572 yield indent(dedent(fhelp.strip()))
573 cfg_list = " ".join(
574 f"--{clname}.{prop}={val}"
575 for clname, props_dict in cfg.items()
576 for prop, val in props_dict.items()
577 )
578 cfg_txt = "Equivalent to: [%s]" % cfg_list
579 yield indent(dedent(cfg_txt))
580 except Exception as ex:
581 self.log.error("Failed collecting help-message for flag %r, due to: %s", flags, ex)
582 raise
584 def print_options(self):
585 """Print the options part of the help."""
586 print("\n".join(self.emit_options_help()))
588 def emit_options_help(self):
589 """Yield the lines for the options part of the help."""
590 if not self.flags and not self.aliases:
591 return
592 header = "Options"
593 yield header
594 yield "=" * len(header)
595 for p in wrap_paragraphs(self.option_description):
596 yield p
597 yield ""
599 yield from self.emit_flag_help()
600 yield from self.emit_alias_help()
601 yield ""
603 def print_subcommands(self):
604 """Print the subcommand part of the help."""
605 print("\n".join(self.emit_subcommands_help()))
607 def emit_subcommands_help(self):
608 """Yield the lines for the subcommand part of the help."""
609 if not self.subcommands:
610 return
612 header = "Subcommands"
613 yield header
614 yield "=" * len(header)
615 for p in wrap_paragraphs(self.subcommand_description.format(app=self.name)):
616 yield p
617 yield ""
618 for subc, (_, help) in self.subcommands.items():
619 yield subc
620 if help:
621 yield indent(dedent(help.strip()))
622 yield ""
624 def emit_help_epilogue(self, classes):
625 """Yield the very bottom lines of the help message.
627 If classes=False (the default), print `--help-all` msg.
628 """
629 if not classes:
630 yield "To see all available configurables, use `--help-all`."
631 yield ""
633 def print_help(self, classes=False):
634 """Print the help for each Configurable class in self.classes.
636 If classes=False (the default), only flags and aliases are printed.
637 """
638 print("\n".join(self.emit_help(classes=classes)))
640 def emit_help(self, classes=False):
641 """Yield the help-lines for each Configurable class in self.classes.
643 If classes=False (the default), only flags and aliases are printed.
644 """
645 yield from self.emit_description()
646 yield from self.emit_subcommands_help()
647 yield from self.emit_options_help()
649 if classes:
650 help_classes = self._classes_with_config_traits()
651 if help_classes:
652 yield "Class options"
653 yield "============="
654 for p in wrap_paragraphs(self.keyvalue_description):
655 yield p
656 yield ""
658 for cls in help_classes:
659 yield cls.class_get_help()
660 yield ""
661 yield from self.emit_examples()
663 yield from self.emit_help_epilogue(classes)
665 def document_config_options(self):
666 """Generate rST format documentation for the config options this application
668 Returns a multiline string.
669 """
670 return "\n".join(c.class_config_rst_doc() for c in self._classes_inc_parents())
672 def print_description(self):
673 """Print the application description."""
674 print("\n".join(self.emit_description()))
676 def emit_description(self):
677 """Yield lines with the application description."""
678 for p in wrap_paragraphs(self.description or self.__doc__ or ""):
679 yield p
680 yield ""
682 def print_examples(self):
683 """Print usage and examples (see `emit_examples()`)."""
684 print("\n".join(self.emit_examples()))
686 def emit_examples(self):
687 """Yield lines with the usage and examples.
689 This usage string goes at the end of the command line help string
690 and should contain examples of the application's usage.
691 """
692 if self.examples:
693 yield "Examples"
694 yield "--------"
695 yield ""
696 yield indent(dedent(self.examples.strip()))
697 yield ""
699 def print_version(self):
700 """Print the version string."""
701 print(self.version)
703 @catch_config_error
704 def initialize_subcommand(self, subc, argv=None):
705 """Initialize a subcommand with argv."""
706 val = self.subcommands.get(subc)
707 assert val is not None
708 subapp, _ = val
710 if isinstance(subapp, str):
711 subapp = import_item(subapp)
713 # Cannot issubclass() on a non-type (SOhttp://stackoverflow.com/questions/8692430)
714 if isinstance(subapp, type) and issubclass(subapp, Application):
715 # Clear existing instances before...
716 self.__class__.clear_instance()
717 # instantiating subapp...
718 self.subapp = subapp.instance(parent=self)
719 elif callable(subapp):
720 # or ask factory to create it...
721 self.subapp = subapp(self) # type:ignore[call-arg]
722 else:
723 raise AssertionError("Invalid mappings for subcommand '%s'!" % subc)
725 # ... and finally initialize subapp.
726 self.subapp.initialize(argv)
728 def flatten_flags(self):
729 """Flatten flags and aliases for loaders, so cl-args override as expected.
731 This prevents issues such as an alias pointing to InteractiveShell,
732 but a config file setting the same trait in TerminalInteraciveShell
733 getting inappropriate priority over the command-line arg.
734 Also, loaders expect ``(key: longname)`` and not ``key: (longname, help)`` items.
736 Only aliases with exactly one descendent in the class list
737 will be promoted.
739 """
740 # build a tree of classes in our list that inherit from a particular
741 # it will be a dict by parent classname of classes in our list
742 # that are descendents
743 mro_tree = defaultdict(list)
744 for cls in self.classes:
745 clsname = cls.__name__
746 for parent in cls.mro()[1:-3]:
747 # exclude cls itself and Configurable,HasTraits,object
748 mro_tree[parent.__name__].append(clsname)
749 # flatten aliases, which have the form:
750 # { 'alias' : 'Class.trait' }
751 aliases: t.Dict[str, str] = {}
752 for alias, longname in self.aliases.items():
753 if isinstance(longname, tuple):
754 longname, _ = longname
755 cls, trait = longname.split(".", 1) # type:ignore
756 children = mro_tree[cls] # type:ignore[index]
757 if len(children) == 1:
758 # exactly one descendent, promote alias
759 cls = children[0] # type:ignore[assignment]
760 if not isinstance(aliases, tuple):
761 alias = (alias,) # type:ignore[assignment]
762 for al in alias:
763 aliases[al] = ".".join([cls, trait]) # type:ignore[list-item]
765 # flatten flags, which are of the form:
766 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
767 flags = {}
768 for key, (flagdict, help) in self.flags.items():
769 newflag: t.Dict[t.Any, t.Any] = {}
770 for cls, subdict in flagdict.items(): # type:ignore
771 children = mro_tree[cls] # type:ignore[index]
772 # exactly one descendent, promote flag section
773 if len(children) == 1:
774 cls = children[0] # type:ignore[assignment]
776 if cls in newflag:
777 newflag[cls].update(subdict)
778 else:
779 newflag[cls] = subdict
781 if not isinstance(key, tuple):
782 key = (key,)
783 for k in key:
784 flags[k] = (newflag, help)
785 return flags, aliases
787 def _create_loader(self, argv, aliases, flags, classes):
788 return KVArgParseConfigLoader(
789 argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands
790 )
792 @classmethod
793 def _get_sys_argv(cls, check_argcomplete: bool = False) -> t.List[str]:
794 """Get `sys.argv` or equivalent from `argcomplete`
796 `argcomplete`'s strategy is to call the python script with no arguments,
797 so ``len(sys.argv) == 1``, and run until the `ArgumentParser` is constructed
798 and determine what completions are available.
800 On the other hand, `traitlet`'s subcommand-handling strategy is to check
801 ``sys.argv[1]`` and see if it matches a subcommand, and if so then dynamically
802 load the subcommand app and initialize it with ``sys.argv[1:]``.
804 This helper method helps to take the current tokens for `argcomplete` and pass
805 them through as `argv`.
806 """
807 if check_argcomplete and "_ARGCOMPLETE" in os.environ:
808 try:
809 from traitlets.config.argcomplete_config import get_argcomplete_cwords
811 cwords = get_argcomplete_cwords()
812 assert cwords is not None
813 return cwords
814 except (ImportError, ModuleNotFoundError):
815 pass
816 return sys.argv
818 @classmethod
819 def _handle_argcomplete_for_subcommand(cls):
820 """Helper for `argcomplete` to recognize `traitlets` subcommands
822 `argcomplete` does not know that `traitlets` has already consumed subcommands,
823 as it only "sees" the final `argparse.ArgumentParser` that is constructed.
824 (Indeed `KVArgParseConfigLoader` does not get passed subcommands at all currently.)
825 We explicitly manipulate the environment variables used internally by `argcomplete`
826 to get it to skip over the subcommand tokens.
827 """
828 if "_ARGCOMPLETE" not in os.environ:
829 return
831 try:
832 from traitlets.config.argcomplete_config import increment_argcomplete_index
834 increment_argcomplete_index()
835 except (ImportError, ModuleNotFoundError):
836 pass
838 @catch_config_error
839 def parse_command_line(self, argv=None):
840 """Parse the command line arguments."""
841 assert not isinstance(argv, str)
842 if argv is None:
843 argv = self._get_sys_argv(check_argcomplete=bool(self.subcommands))[1:]
844 self.argv = [cast_unicode(arg) for arg in argv]
846 if argv and argv[0] == "help":
847 # turn `ipython help notebook` into `ipython notebook -h`
848 argv = argv[1:] + ["-h"]
850 if self.subcommands and len(argv) > 0:
851 # we have subcommands, and one may have been specified
852 subc, subargv = argv[0], argv[1:]
853 if re.match(r"^\w(\-?\w)*$", subc) and subc in self.subcommands:
854 # it's a subcommand, and *not* a flag or class parameter
855 self._handle_argcomplete_for_subcommand()
856 return self.initialize_subcommand(subc, subargv)
858 # Arguments after a '--' argument are for the script IPython may be
859 # about to run, not IPython iteslf. For arguments parsed here (help and
860 # version), we want to only search the arguments up to the first
861 # occurrence of '--', which we're calling interpreted_argv.
862 try:
863 interpreted_argv = argv[: argv.index("--")]
864 except ValueError:
865 interpreted_argv = argv
867 if any(x in interpreted_argv for x in ("-h", "--help-all", "--help")):
868 self.print_help("--help-all" in interpreted_argv)
869 self.exit(0)
871 if "--version" in interpreted_argv or "-V" in interpreted_argv:
872 self.print_version()
873 self.exit(0)
875 # flatten flags&aliases, so cl-args get appropriate priority:
876 flags, aliases = self.flatten_flags()
877 classes = tuple(self._classes_with_config_traits())
878 loader = self._create_loader(argv, aliases, flags, classes=classes)
879 try:
880 self.cli_config = deepcopy(loader.load_config())
881 except SystemExit:
882 # traitlets 5: no longer print help output on error
883 # help output is huge, and comes after the error
884 raise
885 self.update_config(self.cli_config)
886 # store unparsed args in extra_args
887 self.extra_args = loader.extra_args
889 @classmethod
890 def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file_errors=False):
891 """Load config files (py,json) by filename and path.
893 yield each config object in turn.
894 """
896 if not isinstance(path, list):
897 path = [path]
898 for current in reversed(path):
899 # path list is in descending priority order, so load files backwards:
900 pyloader = cls.python_config_loader_class(basefilename + ".py", path=current, log=log)
901 if log:
902 log.debug("Looking for %s in %s", basefilename, current or os.getcwd())
903 jsonloader = cls.json_config_loader_class(basefilename + ".json", path=current, log=log)
904 loaded: t.List[t.Any] = []
905 filenames: t.List[str] = []
906 for loader in [pyloader, jsonloader]:
907 config = None
908 try:
909 config = loader.load_config()
910 except ConfigFileNotFound:
911 pass
912 except Exception:
913 # try to get the full filename, but it will be empty in the
914 # unlikely event that the error raised before filefind finished
915 filename = loader.full_filename or basefilename
916 # problem while running the file
917 if raise_config_file_errors:
918 raise
919 if log:
920 log.error("Exception while loading config file %s", filename, exc_info=True)
921 else:
922 if log:
923 log.debug("Loaded config file: %s", loader.full_filename)
924 if config:
925 for filename, earlier_config in zip(filenames, loaded):
926 collisions = earlier_config.collisions(config)
927 if collisions and log:
928 log.warning(
929 "Collisions detected in {0} and {1} config files."
930 " {1} has higher priority: {2}".format(
931 filename,
932 loader.full_filename,
933 json.dumps(collisions, indent=2),
934 )
935 )
936 yield (config, loader.full_filename)
937 loaded.append(config)
938 filenames.append(loader.full_filename)
940 @property
941 def loaded_config_files(self):
942 """Currently loaded configuration files"""
943 return self._loaded_config_files[:]
945 @catch_config_error
946 def load_config_file(self, filename, path=None):
947 """Load config files by filename and path."""
948 filename, ext = os.path.splitext(filename)
949 new_config = Config()
950 for (config, fname) in self._load_config_files(
951 filename,
952 path=path,
953 log=self.log,
954 raise_config_file_errors=self.raise_config_file_errors,
955 ):
956 new_config.merge(config)
957 if (
958 fname not in self._loaded_config_files
959 ): # only add to list of loaded files if not previously loaded
960 self._loaded_config_files.append(fname)
961 # add self.cli_config to preserve CLI config priority
962 new_config.merge(self.cli_config)
963 self.update_config(new_config)
965 def _classes_with_config_traits(self, classes=None):
966 """
967 Yields only classes with configurable traits, and their subclasses.
969 :param classes:
970 The list of classes to iterate; if not set, uses :attr:`classes`.
972 Thus, produced sample config-file will contain all classes
973 on which a trait-value may be overridden:
975 - either on the class owning the trait,
976 - or on its subclasses, even if those subclasses do not define
977 any traits themselves.
978 """
979 if classes is None:
980 classes = self.classes
982 cls_to_config = OrderedDict(
983 (cls, bool(cls.class_own_traits(config=True)))
984 for cls in self._classes_inc_parents(classes)
985 )
987 def is_any_parent_included(cls):
988 return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__)
990 # Mark "empty" classes for inclusion if their parents own-traits,
991 # and loop until no more classes gets marked.
992 #
993 while True:
994 to_incl_orig = cls_to_config.copy()
995 cls_to_config = OrderedDict(
996 (cls, inc_yes or is_any_parent_included(cls))
997 for cls, inc_yes in cls_to_config.items()
998 )
999 if cls_to_config == to_incl_orig:
1000 break
1001 for cl, inc_yes in cls_to_config.items():
1002 if inc_yes:
1003 yield cl
1005 def generate_config_file(self, classes=None):
1006 """generate default config file from Configurables"""
1007 lines = ["# Configuration file for %s." % self.name]
1008 lines.append("")
1009 lines.append("c = get_config() #" + "noqa")
1010 lines.append("")
1011 classes = self.classes if classes is None else classes
1012 config_classes = list(self._classes_with_config_traits(classes))
1013 for cls in config_classes:
1014 lines.append(cls.class_config_section(config_classes))
1015 return "\n".join(lines)
1017 def close_handlers(self):
1018 if getattr(self, "_logging_configured", False):
1019 # don't attempt to close handlers unless they have been opened
1020 # (note accessing self.log.handlers will create handlers if they
1021 # have not yet been initialised)
1022 for handler in self.log.handlers:
1023 with suppress(Exception):
1024 handler.close()
1025 self._logging_configured = False
1027 def exit(self, exit_status=0):
1028 self.log.debug("Exiting application: %s" % self.name)
1029 self.close_handlers()
1030 sys.exit(exit_status)
1032 def __del__(self):
1033 self.close_handlers()
1035 @classmethod
1036 def launch_instance(cls, argv=None, **kwargs):
1037 """Launch a global instance of this Application
1039 If a global instance already exists, this reinitializes and starts it
1040 """
1041 app = cls.instance(**kwargs)
1042 app.initialize(argv)
1043 app.start()
1046# -----------------------------------------------------------------------------
1047# utility functions, for convenience
1048# -----------------------------------------------------------------------------
1050default_aliases = Application.aliases
1051default_flags = Application.flags
1054def boolean_flag(name, configurable, set_help="", unset_help=""):
1055 """Helper for building basic --trait, --no-trait flags.
1057 Parameters
1058 ----------
1059 name : str
1060 The name of the flag.
1061 configurable : str
1062 The 'Class.trait' string of the trait to be set/unset with the flag
1063 set_help : unicode
1064 help string for --name flag
1065 unset_help : unicode
1066 help string for --no-name flag
1068 Returns
1069 -------
1070 cfg : dict
1071 A dict with two keys: 'name', and 'no-name', for setting and unsetting
1072 the trait, respectively.
1073 """
1074 # default helpstrings
1075 set_help = set_help or "set %s=True" % configurable
1076 unset_help = unset_help or "set %s=False" % configurable
1078 cls, trait = configurable.split(".")
1080 setter = {cls: {trait: True}}
1081 unsetter = {cls: {trait: False}}
1082 return {name: (setter, set_help), "no-" + name: (unsetter, unset_help)}
1085def get_config():
1086 """Get the config object for the global Application instance, if there is one
1088 otherwise return an empty config object
1089 """
1090 if Application.initialized():
1091 return Application.instance().config
1092 else:
1093 return Config()
1096if __name__ == "__main__":
1097 Application.launch_instance()