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