Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/traitlets/config/application.py: 28%
496 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +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[str, t.Union[str, bytes]]] = Unicode("application")
154 # The description of the application that is printed at the beginning
155 # of the help.
156 description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
157 "This is an application."
158 )
159 # default section descriptions
160 option_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
161 option_description
162 )
163 keyvalue_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
164 keyvalue_description
165 )
166 subcommand_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
167 subcommand_description
168 )
170 python_config_loader_class = PyFileConfigLoader
171 json_config_loader_class = JSONFileConfigLoader
173 # The usage and example string that goes at the end of the help string.
174 examples: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode()
176 # A sequence of Configurable subclasses whose config=True attributes will
177 # be exposed at the command line.
178 classes: t.List[t.Type[t.Any]] = []
180 def _classes_inc_parents(self, classes=None):
181 """Iterate through configurable classes, including configurable parents
183 :param classes:
184 The list of classes to iterate; if not set, uses :attr:`classes`.
186 Children should always be after parents, and each class should only be
187 yielded once.
188 """
189 if classes is None:
190 classes = self.classes
192 seen = set()
193 for c in classes:
194 # We want to sort parents before children, so we reverse the MRO
195 for parent in reversed(c.mro()):
196 if issubclass(parent, Configurable) and (parent not in seen):
197 seen.add(parent)
198 yield parent
200 # The version string of this application.
201 version: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode("0.0")
203 # the argv used to initialize the application
204 argv: t.Union[t.List[str], List] = List()
206 # Whether failing to load config files should prevent startup
207 raise_config_file_errors: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool(
208 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR
209 )
211 # The log level for the application
212 log_level: t.Union[str, int, Enum[t.Any, t.Any]] = Enum(
213 (0, 10, 20, 30, 40, 50, "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"),
214 default_value=logging.WARN,
215 help="Set the log level by value or name.",
216 ).tag(config=True)
218 _log_formatter_cls = LevelFormatter
220 log_datefmt: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
221 "%Y-%m-%d %H:%M:%S", help="The date format used by logging formatters for %(asctime)s"
222 ).tag(config=True)
224 log_format: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode(
225 "[%(name)s]%(highlevel)s %(message)s",
226 help="The Logging format template",
227 ).tag(config=True)
229 def get_default_logging_config(self):
230 """Return the base logging configuration.
232 The default is to log to stderr using a StreamHandler, if no default
233 handler already exists.
235 The log handler level starts at logging.WARN, but this can be adjusted
236 by setting the ``log_level`` attribute.
238 The ``logging_config`` trait is merged into this allowing for finer
239 control of logging.
241 """
242 config: t.Dict[str, t.Any] = {
243 "version": 1,
244 "handlers": {
245 "console": {
246 "class": "logging.StreamHandler",
247 "formatter": "console",
248 "level": logging.getLevelName(self.log_level),
249 "stream": "ext://sys.stderr",
250 },
251 },
252 "formatters": {
253 "console": {
254 "class": (
255 f"{self._log_formatter_cls.__module__}"
256 f".{self._log_formatter_cls.__name__}"
257 ),
258 "format": self.log_format,
259 "datefmt": self.log_datefmt,
260 },
261 },
262 "loggers": {
263 self.__class__.__name__: {
264 "level": "DEBUG",
265 "handlers": ["console"],
266 }
267 },
268 "disable_existing_loggers": False,
269 }
271 if IS_PYTHONW:
272 # disable logging
273 # (this should really go to a file, but file-logging is only
274 # hooked up in parallel applications)
275 del config["handlers"]
276 del config["loggers"]
278 return config
280 @observe("log_datefmt", "log_format", "log_level", "logging_config")
281 def _observe_logging_change(self, change):
282 # convert log level strings to ints
283 log_level = self.log_level
284 if isinstance(log_level, str):
285 self.log_level = getattr(logging, log_level)
286 self._configure_logging()
288 @observe("log", type="default")
289 def _observe_logging_default(self, change):
290 self._configure_logging()
292 def _configure_logging(self):
293 config = self.get_default_logging_config()
294 nested_update(config, self.logging_config or {})
295 dictConfig(config)
296 # make a note that we have configured logging
297 self._logging_configured = True
299 @default("log")
300 def _log_default(self):
301 """Start logging for this application."""
302 log = logging.getLogger(self.__class__.__name__)
303 log.propagate = False
304 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
305 while _log:
306 if _log.handlers:
307 return log
308 if not _log.propagate:
309 break
310 else:
311 _log = _log.parent # type:ignore[assignment]
312 return log
314 logging_config = Dict(
315 help="""
316 Configure additional log handlers.
318 The default stderr logs handler is configured by the
319 log_level, log_datefmt and log_format settings.
321 This configuration can be used to configure additional handlers
322 (e.g. to output the log to a file) or for finer control over the
323 default handlers.
325 If provided this should be a logging configuration dictionary, for
326 more information see:
327 https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
329 This dictionary is merged with the base logging configuration which
330 defines the following:
332 * A logging formatter intended for interactive use called
333 ``console``.
334 * A logging handler that writes to stderr called
335 ``console`` which uses the formatter ``console``.
336 * A logger with the name of this application set to ``DEBUG``
337 level.
339 This example adds a new handler that writes to a file:
341 .. code-block:: python
343 c.Application.logging_config = {
344 'handlers': {
345 'file': {
346 'class': 'logging.FileHandler',
347 'level': 'DEBUG',
348 'filename': '<path/to/file>',
349 }
350 },
351 'loggers': {
352 '<application-name>': {
353 'level': 'DEBUG',
354 # NOTE: if you don't list the default "console"
355 # handler here then it will be disabled
356 'handlers': ['console', 'file'],
357 },
358 }
359 }
361 """,
362 ).tag(config=True)
364 #: the alias map for configurables
365 #: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`.
366 #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text),
367 # or just the "Class.trait" string, in which case the help text is inferred from the
368 # corresponding trait
369 aliases: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Union[str, t.Tuple[str, str]]] = {
370 "log-level": "Application.log_level"
371 }
373 # flags for loading Configurables or store_const style flags
374 # flags are loaded from this dict by '--key' flags
375 # this must be a dict of two-tuples, the first element being the Config/dict
376 # and the second being the help string for the flag
377 flags: t.Dict[
378 t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict[str, t.Any], Config], str]
379 ] = {
380 "debug": (
381 {
382 "Application": {
383 "log_level": logging.DEBUG,
384 },
385 },
386 "Set log-level to debug, for the most verbose logging.",
387 ),
388 "show-config": (
389 {
390 "Application": {
391 "show_config": True,
392 },
393 },
394 "Show the application's configuration (human-readable format)",
395 ),
396 "show-config-json": (
397 {
398 "Application": {
399 "show_config_json": True,
400 },
401 },
402 "Show the application's configuration (json format)",
403 ),
404 }
406 # subcommands for launching other applications
407 # if this is not empty, this will be a parent Application
408 # this must be a dict of two-tuples,
409 # the first element being the application class/import string
410 # and the second being the help string for the subcommand
411 subcommands: t.Union[t.Dict[str, t.Tuple[t.Any, str]], Dict] = Dict()
412 # parse_command_line will initialize a subapp, if requested
413 subapp = Instance("traitlets.config.application.Application", allow_none=True)
415 # extra command-line arguments that don't set config values
416 extra_args: t.Union[t.List[str], List] = List(Unicode())
418 cli_config = Instance(
419 Config,
420 (),
421 {},
422 help="""The subset of our configuration that came from the command-line
424 We re-load this configuration after loading config files,
425 to ensure that it maintains highest priority.
426 """,
427 )
429 _loaded_config_files = List()
431 show_config: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool(
432 help="Instead of starting the Application, dump configuration to stdout"
433 ).tag(config=True)
435 show_config_json: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool(
436 help="Instead of starting the Application, dump configuration to stdout (as JSON)"
437 ).tag(config=True)
439 @observe("show_config_json")
440 def _show_config_json_changed(self, change):
441 self.show_config = change.new
443 @observe("show_config")
444 def _show_config_changed(self, change):
445 if change.new:
446 self._save_start = self.start
447 self.start = self.start_show_config # type:ignore[method-assign]
449 def __init__(self, **kwargs):
450 SingletonConfigurable.__init__(self, **kwargs)
451 # Ensure my class is in self.classes, so my attributes appear in command line
452 # options and config files.
453 cls = self.__class__
454 if cls not in self.classes:
455 if self.classes is cls.classes:
456 # class attr, assign instead of insert
457 self.classes = [cls, *self.classes]
458 else:
459 self.classes.insert(0, self.__class__)
461 @observe("config")
462 @observe_compat
463 def _config_changed(self, change):
464 super()._config_changed(change)
465 self.log.debug("Config changed: %r", change.new)
467 @catch_config_error
468 def initialize(self, argv=None):
469 """Do the basic steps to configure me.
471 Override in subclasses.
472 """
473 self.parse_command_line(argv)
475 def start(self):
476 """Start the app mainloop.
478 Override in subclasses.
479 """
480 if self.subapp is not None:
481 return self.subapp.start()
483 def start_show_config(self):
484 """start function used when show_config is True"""
485 config = self.config.copy()
486 # exclude show_config flags from displayed config
487 for cls in self.__class__.mro():
488 if cls.__name__ in config:
489 cls_config = config[cls.__name__]
490 cls_config.pop("show_config", None)
491 cls_config.pop("show_config_json", None)
493 if self.show_config_json:
494 json.dump(config, sys.stdout, indent=1, sort_keys=True, default=repr)
495 # add trailing newline
496 sys.stdout.write("\n")
497 return
499 if self._loaded_config_files:
500 print("Loaded config files:")
501 for f in self._loaded_config_files:
502 print(" " + f)
503 print()
505 for classname in sorted(config):
506 class_config = config[classname]
507 if not class_config:
508 continue
509 print(classname)
510 pformat_kwargs: t.Dict[str, t.Any] = dict(indent=4, compact=True)
512 for traitname in sorted(class_config):
513 value = class_config[traitname]
514 print(f" .{traitname} = {pprint.pformat(value, **pformat_kwargs)}")
516 def print_alias_help(self):
517 """Print the alias parts of the help."""
518 print("\n".join(self.emit_alias_help()))
520 def emit_alias_help(self):
521 """Yield the lines for alias part of the help."""
522 if not self.aliases:
523 return
525 classdict = {}
526 for cls in self.classes:
527 # include all parents (up to, but excluding Configurable) in available names
528 for c in cls.mro()[:-3]:
529 classdict[c.__name__] = c
531 fhelp: t.Optional[str]
532 for alias, longname in self.aliases.items():
533 try:
534 if isinstance(longname, tuple):
535 longname, fhelp = longname
536 else:
537 fhelp = None
538 classname, traitname = longname.split(".")[-2:]
539 longname = classname + "." + traitname
540 cls = classdict[classname]
542 trait = cls.class_traits(config=True)[traitname]
543 fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines()
545 if not isinstance(alias, tuple):
546 alias = (alias,)
547 alias = sorted(alias, key=len) # type:ignore[assignment]
548 alias = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in alias)
550 # reformat first line
551 assert fhelp is not None
552 fhelp[0] = fhelp[0].replace("--" + longname, alias) # type:ignore
553 yield from fhelp
554 yield indent("Equivalent to: [--%s]" % longname)
555 except Exception as ex:
556 self.log.error("Failed collecting help-message for alias %r, due to: %s", alias, ex)
557 raise
559 def print_flag_help(self):
560 """Print the flag part of the help."""
561 print("\n".join(self.emit_flag_help()))
563 def emit_flag_help(self):
564 """Yield the lines for the flag part of the help."""
565 if not self.flags:
566 return
568 for flags, (cfg, fhelp) in self.flags.items():
569 try:
570 if not isinstance(flags, tuple):
571 flags = (flags,)
572 flags = sorted(flags, key=len) # type:ignore[assignment]
573 flags = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in flags)
574 yield flags
575 yield indent(dedent(fhelp.strip()))
576 cfg_list = " ".join(
577 f"--{clname}.{prop}={val}"
578 for clname, props_dict in cfg.items()
579 for prop, val in props_dict.items()
580 )
581 cfg_txt = "Equivalent to: [%s]" % cfg_list
582 yield indent(dedent(cfg_txt))
583 except Exception as ex:
584 self.log.error("Failed collecting help-message for flag %r, due to: %s", flags, ex)
585 raise
587 def print_options(self):
588 """Print the options part of the help."""
589 print("\n".join(self.emit_options_help()))
591 def emit_options_help(self):
592 """Yield the lines for the options part of the help."""
593 if not self.flags and not self.aliases:
594 return
595 header = "Options"
596 yield header
597 yield "=" * len(header)
598 for p in wrap_paragraphs(self.option_description):
599 yield p
600 yield ""
602 yield from self.emit_flag_help()
603 yield from self.emit_alias_help()
604 yield ""
606 def print_subcommands(self):
607 """Print the subcommand part of the help."""
608 print("\n".join(self.emit_subcommands_help()))
610 def emit_subcommands_help(self):
611 """Yield the lines for the subcommand part of the help."""
612 if not self.subcommands:
613 return
615 header = "Subcommands"
616 yield header
617 yield "=" * len(header)
618 for p in wrap_paragraphs(self.subcommand_description.format(app=self.name)):
619 yield p
620 yield ""
621 for subc, (_, help) in self.subcommands.items():
622 yield subc
623 if help:
624 yield indent(dedent(help.strip()))
625 yield ""
627 def emit_help_epilogue(self, classes):
628 """Yield the very bottom lines of the help message.
630 If classes=False (the default), print `--help-all` msg.
631 """
632 if not classes:
633 yield "To see all available configurables, use `--help-all`."
634 yield ""
636 def print_help(self, classes=False):
637 """Print the help for each Configurable class in self.classes.
639 If classes=False (the default), only flags and aliases are printed.
640 """
641 print("\n".join(self.emit_help(classes=classes)))
643 def emit_help(self, classes=False):
644 """Yield the help-lines for each Configurable class in self.classes.
646 If classes=False (the default), only flags and aliases are printed.
647 """
648 yield from self.emit_description()
649 yield from self.emit_subcommands_help()
650 yield from self.emit_options_help()
652 if classes:
653 help_classes = self._classes_with_config_traits()
654 if help_classes:
655 yield "Class options"
656 yield "============="
657 for p in wrap_paragraphs(self.keyvalue_description):
658 yield p
659 yield ""
661 for cls in help_classes:
662 yield cls.class_get_help()
663 yield ""
664 yield from self.emit_examples()
666 yield from self.emit_help_epilogue(classes)
668 def document_config_options(self):
669 """Generate rST format documentation for the config options this application
671 Returns a multiline string.
672 """
673 return "\n".join(c.class_config_rst_doc() for c in self._classes_inc_parents())
675 def print_description(self):
676 """Print the application description."""
677 print("\n".join(self.emit_description()))
679 def emit_description(self):
680 """Yield lines with the application description."""
681 for p in wrap_paragraphs(self.description or self.__doc__ or ""):
682 yield p
683 yield ""
685 def print_examples(self):
686 """Print usage and examples (see `emit_examples()`)."""
687 print("\n".join(self.emit_examples()))
689 def emit_examples(self):
690 """Yield lines with the usage and examples.
692 This usage string goes at the end of the command line help string
693 and should contain examples of the application's usage.
694 """
695 if self.examples:
696 yield "Examples"
697 yield "--------"
698 yield ""
699 yield indent(dedent(self.examples.strip()))
700 yield ""
702 def print_version(self):
703 """Print the version string."""
704 print(self.version)
706 @catch_config_error
707 def initialize_subcommand(self, subc, argv=None):
708 """Initialize a subcommand with argv."""
709 val = self.subcommands.get(subc)
710 assert val is not None
711 subapp, _ = val
713 if isinstance(subapp, str):
714 subapp = import_item(subapp)
716 # Cannot issubclass() on a non-type (SOhttp://stackoverflow.com/questions/8692430)
717 if isinstance(subapp, type) and issubclass(subapp, Application):
718 # Clear existing instances before...
719 self.__class__.clear_instance()
720 # instantiating subapp...
721 self.subapp = subapp.instance(parent=self)
722 elif callable(subapp):
723 # or ask factory to create it...
724 self.subapp = subapp(self) # type:ignore[call-arg]
725 else:
726 raise AssertionError("Invalid mappings for subcommand '%s'!" % subc)
728 # ... and finally initialize subapp.
729 self.subapp.initialize(argv)
731 def flatten_flags(self):
732 """Flatten flags and aliases for loaders, so cl-args override as expected.
734 This prevents issues such as an alias pointing to InteractiveShell,
735 but a config file setting the same trait in TerminalInteraciveShell
736 getting inappropriate priority over the command-line arg.
737 Also, loaders expect ``(key: longname)`` and not ``key: (longname, help)`` items.
739 Only aliases with exactly one descendent in the class list
740 will be promoted.
742 """
743 # build a tree of classes in our list that inherit from a particular
744 # it will be a dict by parent classname of classes in our list
745 # that are descendents
746 mro_tree = defaultdict(list)
747 for cls in self.classes:
748 clsname = cls.__name__
749 for parent in cls.mro()[1:-3]:
750 # exclude cls itself and Configurable,HasTraits,object
751 mro_tree[parent.__name__].append(clsname)
752 # flatten aliases, which have the form:
753 # { 'alias' : 'Class.trait' }
754 aliases: t.Dict[str, str] = {}
755 for alias, longname in self.aliases.items():
756 if isinstance(longname, tuple):
757 longname, _ = longname
758 cls, trait = longname.split(".", 1) # type:ignore
759 children = mro_tree[cls] # type:ignore[index]
760 if len(children) == 1:
761 # exactly one descendent, promote alias
762 cls = children[0] # type:ignore[assignment]
763 if not isinstance(aliases, tuple):
764 alias = (alias,) # type:ignore[assignment]
765 for al in alias:
766 aliases[al] = ".".join([cls, trait]) # type:ignore[list-item]
768 # flatten flags, which are of the form:
769 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
770 flags = {}
771 for key, (flagdict, help) in self.flags.items():
772 newflag: t.Dict[t.Any, t.Any] = {}
773 for cls, subdict in flagdict.items(): # type:ignore
774 children = mro_tree[cls] # type:ignore[index]
775 # exactly one descendent, promote flag section
776 if len(children) == 1:
777 cls = children[0] # type:ignore[assignment]
779 if cls in newflag:
780 newflag[cls].update(subdict)
781 else:
782 newflag[cls] = subdict
784 if not isinstance(key, tuple):
785 key = (key,)
786 for k in key:
787 flags[k] = (newflag, help)
788 return flags, aliases
790 def _create_loader(self, argv, aliases, flags, classes):
791 return KVArgParseConfigLoader(
792 argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands
793 )
795 @classmethod
796 def _get_sys_argv(cls, check_argcomplete: bool = False) -> t.List[str]:
797 """Get `sys.argv` or equivalent from `argcomplete`
799 `argcomplete`'s strategy is to call the python script with no arguments,
800 so ``len(sys.argv) == 1``, and run until the `ArgumentParser` is constructed
801 and determine what completions are available.
803 On the other hand, `traitlet`'s subcommand-handling strategy is to check
804 ``sys.argv[1]`` and see if it matches a subcommand, and if so then dynamically
805 load the subcommand app and initialize it with ``sys.argv[1:]``.
807 This helper method helps to take the current tokens for `argcomplete` and pass
808 them through as `argv`.
809 """
810 if check_argcomplete and "_ARGCOMPLETE" in os.environ:
811 try:
812 from traitlets.config.argcomplete_config import get_argcomplete_cwords
814 cwords = get_argcomplete_cwords()
815 assert cwords is not None
816 return cwords
817 except (ImportError, ModuleNotFoundError):
818 pass
819 return sys.argv
821 @classmethod
822 def _handle_argcomplete_for_subcommand(cls):
823 """Helper for `argcomplete` to recognize `traitlets` subcommands
825 `argcomplete` does not know that `traitlets` has already consumed subcommands,
826 as it only "sees" the final `argparse.ArgumentParser` that is constructed.
827 (Indeed `KVArgParseConfigLoader` does not get passed subcommands at all currently.)
828 We explicitly manipulate the environment variables used internally by `argcomplete`
829 to get it to skip over the subcommand tokens.
830 """
831 if "_ARGCOMPLETE" not in os.environ:
832 return
834 try:
835 from traitlets.config.argcomplete_config import increment_argcomplete_index
837 increment_argcomplete_index()
838 except (ImportError, ModuleNotFoundError):
839 pass
841 @catch_config_error
842 def parse_command_line(self, argv=None):
843 """Parse the command line arguments."""
844 assert not isinstance(argv, str)
845 if argv is None:
846 argv = self._get_sys_argv(check_argcomplete=bool(self.subcommands))[1:]
847 self.argv = [cast_unicode(arg) for arg in argv]
849 if argv and argv[0] == "help":
850 # turn `ipython help notebook` into `ipython notebook -h`
851 argv = argv[1:] + ["-h"]
853 if self.subcommands and len(argv) > 0:
854 # we have subcommands, and one may have been specified
855 subc, subargv = argv[0], argv[1:]
856 if re.match(r"^\w(\-?\w)*$", subc) and subc in self.subcommands:
857 # it's a subcommand, and *not* a flag or class parameter
858 self._handle_argcomplete_for_subcommand()
859 return self.initialize_subcommand(subc, subargv)
861 # Arguments after a '--' argument are for the script IPython may be
862 # about to run, not IPython iteslf. For arguments parsed here (help and
863 # version), we want to only search the arguments up to the first
864 # occurrence of '--', which we're calling interpreted_argv.
865 try:
866 interpreted_argv = argv[: argv.index("--")]
867 except ValueError:
868 interpreted_argv = argv
870 if any(x in interpreted_argv for x in ("-h", "--help-all", "--help")):
871 self.print_help("--help-all" in interpreted_argv)
872 self.exit(0)
874 if "--version" in interpreted_argv or "-V" in interpreted_argv:
875 self.print_version()
876 self.exit(0)
878 # flatten flags&aliases, so cl-args get appropriate priority:
879 flags, aliases = self.flatten_flags()
880 classes = tuple(self._classes_with_config_traits())
881 loader = self._create_loader(argv, aliases, flags, classes=classes)
882 try:
883 self.cli_config = deepcopy(loader.load_config())
884 except SystemExit:
885 # traitlets 5: no longer print help output on error
886 # help output is huge, and comes after the error
887 raise
888 self.update_config(self.cli_config)
889 # store unparsed args in extra_args
890 self.extra_args = loader.extra_args
892 @classmethod
893 def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file_errors=False):
894 """Load config files (py,json) by filename and path.
896 yield each config object in turn.
897 """
899 if not isinstance(path, list):
900 path = [path]
901 for current in reversed(path):
902 # path list is in descending priority order, so load files backwards:
903 pyloader = cls.python_config_loader_class(basefilename + ".py", path=current, log=log)
904 if log:
905 log.debug("Looking for %s in %s", basefilename, current or os.getcwd())
906 jsonloader = cls.json_config_loader_class(basefilename + ".json", path=current, log=log)
907 loaded: t.List[t.Any] = []
908 filenames: t.List[str] = []
909 for loader in [pyloader, jsonloader]:
910 config = None
911 try:
912 config = loader.load_config()
913 except ConfigFileNotFound:
914 pass
915 except Exception:
916 # try to get the full filename, but it will be empty in the
917 # unlikely event that the error raised before filefind finished
918 filename = loader.full_filename or basefilename
919 # problem while running the file
920 if raise_config_file_errors:
921 raise
922 if log:
923 log.error("Exception while loading config file %s", filename, exc_info=True)
924 else:
925 if log:
926 log.debug("Loaded config file: %s", loader.full_filename)
927 if config:
928 for filename, earlier_config in zip(filenames, loaded):
929 collisions = earlier_config.collisions(config)
930 if collisions and log:
931 log.warning(
932 "Collisions detected in {0} and {1} config files."
933 " {1} has higher priority: {2}".format(
934 filename,
935 loader.full_filename,
936 json.dumps(collisions, indent=2),
937 )
938 )
939 yield (config, loader.full_filename)
940 loaded.append(config)
941 filenames.append(loader.full_filename)
943 @property
944 def loaded_config_files(self):
945 """Currently loaded configuration files"""
946 return self._loaded_config_files[:]
948 @catch_config_error
949 def load_config_file(self, filename, path=None):
950 """Load config files by filename and path."""
951 filename, ext = os.path.splitext(filename)
952 new_config = Config()
953 for config, fname in self._load_config_files(
954 filename,
955 path=path,
956 log=self.log,
957 raise_config_file_errors=self.raise_config_file_errors,
958 ):
959 new_config.merge(config)
960 if (
961 fname not in self._loaded_config_files
962 ): # only add to list of loaded files if not previously loaded
963 self._loaded_config_files.append(fname)
964 # add self.cli_config to preserve CLI config priority
965 new_config.merge(self.cli_config)
966 self.update_config(new_config)
968 def _classes_with_config_traits(self, classes=None):
969 """
970 Yields only classes with configurable traits, and their subclasses.
972 :param classes:
973 The list of classes to iterate; if not set, uses :attr:`classes`.
975 Thus, produced sample config-file will contain all classes
976 on which a trait-value may be overridden:
978 - either on the class owning the trait,
979 - or on its subclasses, even if those subclasses do not define
980 any traits themselves.
981 """
982 if classes is None:
983 classes = self.classes
985 cls_to_config = OrderedDict(
986 (cls, bool(cls.class_own_traits(config=True)))
987 for cls in self._classes_inc_parents(classes)
988 )
990 def is_any_parent_included(cls):
991 return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__)
993 # Mark "empty" classes for inclusion if their parents own-traits,
994 # and loop until no more classes gets marked.
995 #
996 while True:
997 to_incl_orig = cls_to_config.copy()
998 cls_to_config = OrderedDict(
999 (cls, inc_yes or is_any_parent_included(cls))
1000 for cls, inc_yes in cls_to_config.items()
1001 )
1002 if cls_to_config == to_incl_orig:
1003 break
1004 for cl, inc_yes in cls_to_config.items():
1005 if inc_yes:
1006 yield cl
1008 def generate_config_file(self, classes=None):
1009 """generate default config file from Configurables"""
1010 lines = ["# Configuration file for %s." % self.name]
1011 lines.append("")
1012 lines.append("c = get_config() #" + "noqa")
1013 lines.append("")
1014 classes = self.classes if classes is None else classes
1015 config_classes = list(self._classes_with_config_traits(classes))
1016 for cls in config_classes:
1017 lines.append(cls.class_config_section(config_classes))
1018 return "\n".join(lines)
1020 def close_handlers(self):
1021 if getattr(self, "_logging_configured", False):
1022 # don't attempt to close handlers unless they have been opened
1023 # (note accessing self.log.handlers will create handlers if they
1024 # have not yet been initialised)
1025 for handler in self.log.handlers:
1026 with suppress(Exception):
1027 handler.close()
1028 self._logging_configured = False
1030 def exit(self, exit_status=0):
1031 self.log.debug("Exiting application: %s" % self.name)
1032 self.close_handlers()
1033 sys.exit(exit_status)
1035 def __del__(self):
1036 self.close_handlers()
1038 @classmethod
1039 def launch_instance(cls, argv=None, **kwargs):
1040 """Launch a global instance of this Application
1042 If a global instance already exists, this reinitializes and starts it
1043 """
1044 app = cls.instance(**kwargs)
1045 app.initialize(argv)
1046 app.start()
1049# -----------------------------------------------------------------------------
1050# utility functions, for convenience
1051# -----------------------------------------------------------------------------
1053default_aliases = Application.aliases
1054default_flags = Application.flags
1057def boolean_flag(name, configurable, set_help="", unset_help=""):
1058 """Helper for building basic --trait, --no-trait flags.
1060 Parameters
1061 ----------
1062 name : str
1063 The name of the flag.
1064 configurable : str
1065 The 'Class.trait' string of the trait to be set/unset with the flag
1066 set_help : unicode
1067 help string for --name flag
1068 unset_help : unicode
1069 help string for --no-name flag
1071 Returns
1072 -------
1073 cfg : dict
1074 A dict with two keys: 'name', and 'no-name', for setting and unsetting
1075 the trait, respectively.
1076 """
1077 # default helpstrings
1078 set_help = set_help or "set %s=True" % configurable
1079 unset_help = unset_help or "set %s=False" % configurable
1081 cls, trait = configurable.split(".")
1083 setter = {cls: {trait: True}}
1084 unsetter = {cls: {trait: False}}
1085 return {name: (setter, set_help), "no-" + name: (unsetter, unset_help)}
1088def get_config():
1089 """Get the config object for the global Application instance, if there is one
1091 otherwise return an empty config object
1092 """
1093 if Application.initialized():
1094 return Application.instance().config
1095 else:
1096 return Config()
1099if __name__ == "__main__":
1100 Application.launch_instance()