application.py 42 KB

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