Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/traitlets/config/application.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

521 statements  

1"""A base class for a configurable application.""" 

2 

3# Copyright (c) IPython Development Team. 

4# Distributed under the terms of the Modified BSD License. 

5from __future__ import annotations 

6 

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 

20 

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 

46 

47from ..utils import cast_unicode 

48from ..utils.importstring import import_item 

49 

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 

60 

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 

68 

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 = [""] 

73 

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 

79 

80# ----------------------------------------------------------------------------- 

81# Application class 

82# ----------------------------------------------------------------------------- 

83 

84 

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 ) 

95 

96 

97IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe") 

98 

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]] 

104 

105 

106def catch_config_error(method: T) -> T: 

107 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init. 

108 

109 On a TraitError (generally caused by bad config), this will print the trait's 

110 message, and exit the app. 

111 

112 For use on init methods, to prevent invoking excepthook on invalid input. 

113 """ 

114 

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) 

123 

124 return t.cast(T, inner) 

125 

126 

127class ApplicationError(Exception): 

128 pass 

129 

130 

131class LevelFormatter(logging.Formatter): 

132 """Formatter with additional `highlevel` record 

133 

134 This field is empty if log level is less than highlevel_limit, 

135 otherwise it is formatted with self.highlevel_format. 

136 

137 Useful for adding 'WARNING' to warning messages, 

138 without adding 'INFO' to info, etc. 

139 """ 

140 

141 highlevel_limit = logging.WARN 

142 highlevel_format = " %(levelname)s |" 

143 

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) 

150 

151 

152class Application(SingletonConfigurable): 

153 """A singleton application with full configuration support.""" 

154 

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") 

158 

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) 

166 

167 python_config_loader_class = PyFileConfigLoader 

168 json_config_loader_class = JSONFileConfigLoader 

169 

170 # The usage and example string that goes at the end of the help string. 

171 examples: str | Unicode[str, str | bytes] = Unicode() 

172 

173 # A sequence of Configurable subclasses whose config=True attributes will 

174 # be exposed at the command line. 

175 classes: ClassesType = [] 

176 

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 

181 

182 :param classes: 

183 The list of classes to iterate; if not set, uses :attr:`classes`. 

184 

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 

190 

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 

198 

199 # The version string of this application. 

200 version: str | Unicode[str, str | bytes] = Unicode("0.0") 

201 

202 # the argv used to initialize the application 

203 argv: list[str] | List[str] = List() 

204 

205 # Whether failing to load config files should prevent startup 

206 raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR) 

207 

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) 

214 

215 _log_formatter_cls = LevelFormatter 

216 

217 log_datefmt = Unicode( 

218 "%Y-%m-%d %H:%M:%S", 

219 help="The date format used by logging formatters for `asctime`", 

220 ).tag(config=True) 

221 

222 log_format = Unicode( 

223 "[%(name)s]%(highlevel)s %(message)s", 

224 help="The Logging format template", 

225 ).tag(config=True) 

226 

227 def get_default_logging_config(self) -> StrDict: 

228 """Return the base logging configuration. 

229 

230 The default is to log to stderr using a StreamHandler, if no default 

231 handler already exists. 

232 

233 The log handler level starts at logging.WARN, but this can be adjusted 

234 by setting the ``log_level`` attribute. 

235 

236 The ``logging_config`` trait is merged into this allowing for finer 

237 control of logging. 

238 

239 """ 

240 config: StrDict = { 

241 "version": 1, 

242 "handlers": { 

243 "console": { 

244 "class": "logging.StreamHandler", 

245 "formatter": "console", 

246 "level": logging.getLevelName(self.log_level), # type:ignore[arg-type] 

247 "stream": "ext://sys.stderr", 

248 }, 

249 }, 

250 "formatters": { 

251 "console": { 

252 "class": ( 

253 f"{self._log_formatter_cls.__module__}" 

254 f".{self._log_formatter_cls.__name__}" 

255 ), 

256 "format": self.log_format, 

257 "datefmt": self.log_datefmt, 

258 }, 

259 }, 

260 "loggers": { 

261 self.__class__.__name__: { 

262 "level": "DEBUG", 

263 "handlers": ["console"], 

264 } 

265 }, 

266 "disable_existing_loggers": False, 

267 } 

268 

269 if IS_PYTHONW: 

270 # disable logging 

271 # (this should really go to a file, but file-logging is only 

272 # hooked up in parallel applications) 

273 del config["handlers"] 

274 del config["loggers"] 

275 

276 return config 

277 

278 @observe("log_datefmt", "log_format", "log_level", "logging_config") 

279 def _observe_logging_change(self, change: Bunch) -> None: 

280 # convert log level strings to ints 

281 log_level = self.log_level 

282 if isinstance(log_level, str): 

283 self.log_level = t.cast(int, getattr(logging, log_level)) 

284 self._configure_logging() 

285 

286 @observe("log", type="default") 

287 def _observe_logging_default(self, change: Bunch) -> None: 

288 self._configure_logging() 

289 

290 def _configure_logging(self) -> None: 

291 config = self.get_default_logging_config() 

292 nested_update(config, self.logging_config or {}) 

293 dictConfig(config) 

294 # make a note that we have configured logging 

295 self._logging_configured = True 

296 

297 @default("log") 

298 def _log_default(self) -> AnyLogger: 

299 """Start logging for this application.""" 

300 log = logging.getLogger(self.__class__.__name__) 

301 log.propagate = False 

302 _log = log # copied from Logger.hasHandlers() (new in Python 3.2) 

303 while _log is not None: 

304 if _log.handlers: 

305 return log 

306 if not _log.propagate: 

307 break 

308 _log = _log.parent # type:ignore[assignment] 

309 return log 

310 

311 logging_config = Dict( 

312 help=""" 

313 Configure additional log handlers. 

314 

315 The default stderr logs handler is configured by the 

316 log_level, log_datefmt and log_format settings. 

317 

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. 

321 

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 

325 

326 This dictionary is merged with the base logging configuration which 

327 defines the following: 

328 

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. 

335 

336 This example adds a new handler that writes to a file: 

337 

338 .. code-block:: python 

339 

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 } 

357 

358 """, 

359 ).tag(config=True) 

360 

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"} 

367 

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 } 

398 

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) 

407 

408 # extra command-line arguments that don't set config values 

409 extra_args = List(Unicode()) 

410 

411 cli_config = Instance( 

412 Config, 

413 (), 

414 {}, 

415 help="""The subset of our configuration that came from the command-line 

416 

417 We re-load this configuration after loading config files, 

418 to ensure that it maintains highest priority. 

419 """, 

420 ) 

421 

422 _loaded_config_files: List[str] = List() 

423 

424 show_config = Bool( 

425 help="Instead of starting the Application, dump configuration to stdout" 

426 ).tag(config=True) 

427 

428 show_config_json = Bool( 

429 help="Instead of starting the Application, dump configuration to stdout (as JSON)" 

430 ).tag(config=True) 

431 

432 @observe("show_config_json") 

433 def _show_config_json_changed(self, change: Bunch) -> None: 

434 self.show_config = change.new 

435 

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] 

441 

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__) 

453 

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) 

459 

460 @catch_config_error 

461 def initialize(self, argv: ArgvType = None) -> None: 

462 """Do the basic steps to configure me. 

463 

464 Override in subclasses. 

465 """ 

466 self.parse_command_line(argv) 

467 

468 def start(self) -> None: 

469 """Start the app mainloop. 

470 

471 Override in subclasses. 

472 """ 

473 if self.subapp is not None: 

474 assert isinstance(self.subapp, Application) 

475 return self.subapp.start() 

476 

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) 

486 

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 

492 

493 if self._loaded_config_files: 

494 print("Loaded config files:") 

495 for f in self._loaded_config_files: 

496 print(" " + f) 

497 print() 

498 

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) # noqa: C408 

505 

506 for traitname in sorted(class_config): 

507 value = class_config[traitname] 

508 print(f" .{traitname} = {pprint.pformat(value, **pformat_kwargs)}") 

509 

510 def print_alias_help(self) -> None: 

511 """Print the alias parts of the help.""" 

512 print("\n".join(self.emit_alias_help())) 

513 

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 

518 

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) 

524 

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] 

535 

536 trait = cls.class_traits(config=True)[traitname] 

537 fhelp_lines = cls.class_get_trait_help(trait, helptext=fhelp).splitlines() 

538 

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) 

543 

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 

551 

552 def print_flag_help(self) -> None: 

553 """Print the flag part of the help.""" 

554 print("\n".join(self.emit_flag_help())) 

555 

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 

560 

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 

579 

580 def print_options(self) -> None: 

581 """Print the options part of the help.""" 

582 print("\n".join(self.emit_options_help())) 

583 

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 "" 

594 

595 yield from self.emit_flag_help() 

596 yield from self.emit_alias_help() 

597 yield "" 

598 

599 def print_subcommands(self) -> None: 

600 """Print the subcommand part of the help.""" 

601 print("\n".join(self.emit_subcommands_help())) 

602 

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 

607 

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 "" 

619 

620 def emit_help_epilogue(self, classes: bool) -> t.Generator[str, None, None]: 

621 """Yield the very bottom lines of the help message. 

622 

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 "" 

628 

629 def print_help(self, classes: bool = False) -> None: 

630 """Print the help for each Configurable class in self.classes. 

631 

632 If classes=False (the default), only flags and aliases are printed. 

633 """ 

634 print("\n".join(self.emit_help(classes=classes))) 

635 

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. 

638 

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() 

644 

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 "" 

653 

654 for cls in help_classes: 

655 yield cls.class_get_help() 

656 yield "" 

657 yield from self.emit_examples() 

658 

659 yield from self.emit_help_epilogue(classes) 

660 

661 def document_config_options(self) -> str: 

662 """Generate rST format documentation for the config options this application 

663 

664 Returns a multiline string. 

665 """ 

666 return "\n".join(c.class_config_rst_doc() for c in self._classes_inc_parents()) 

667 

668 def print_description(self) -> None: 

669 """Print the application description.""" 

670 print("\n".join(self.emit_description())) 

671 

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 "" 

677 

678 def print_examples(self) -> None: 

679 """Print usage and examples (see `emit_examples()`).""" 

680 print("\n".join(self.emit_examples())) 

681 

682 def emit_examples(self) -> t.Generator[str, None, None]: 

683 """Yield lines with the usage and examples. 

684 

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 "" 

694 

695 def print_version(self) -> None: 

696 """Print the version string.""" 

697 print(self.version) 

698 

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 

705 

706 if isinstance(subapp, str): 

707 subapp = import_item(subapp) 

708 

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) 

720 

721 # ... and finally initialize subapp. 

722 self.subapp.initialize(argv) 

723 

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. 

726 

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. 

731 

732 Only aliases with exactly one descendent in the class list 

733 will be promoted. 

734 

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] 

760 

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] 

771 

772 if cls in newflag: 

773 newflag[cls].update(subdict) 

774 else: 

775 newflag[cls] = subdict 

776 

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 

782 

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 ) 

793 

794 @classmethod 

795 def _get_sys_argv(cls, check_argcomplete: bool = False) -> list[str]: 

796 """Get `sys.argv` or equivalent from `argcomplete` 

797 

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. 

801 

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:]``. 

805 

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 

812 

813 cwords = get_argcomplete_cwords() 

814 assert cwords is not None 

815 return cwords 

816 except (ImportError, ModuleNotFoundError): 

817 pass 

818 return sys.argv 

819 

820 @classmethod 

821 def _handle_argcomplete_for_subcommand(cls) -> None: 

822 """Helper for `argcomplete` to recognize `traitlets` subcommands 

823 

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 

832 

833 try: 

834 from traitlets.config.argcomplete_config import increment_argcomplete_index 

835 

836 increment_argcomplete_index() 

837 except (ImportError, ModuleNotFoundError): 

838 pass 

839 

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] 

847 

848 if argv and argv[0] == "help": 

849 # turn `ipython help notebook` into `ipython notebook -h` 

850 argv = argv[1:] + ["-h"] 

851 

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) 

859 

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 

868 

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) 

872 

873 if "--version" in interpreted_argv or "-V" in interpreted_argv: 

874 self.print_version() 

875 self.exit(0) 

876 

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 

890 

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. 

900 

901 yield each config object in turn. 

902 """ 

903 if os.path.isabs(basefilename): 

904 path = [None] 

905 if isinstance(path, str) or path is None: 

906 path = [path] 

907 for current in reversed(path): 

908 # path list is in descending priority order, so load files backwards: 

909 pyloader = cls.python_config_loader_class(basefilename + ".py", path=current, log=log) 

910 if log: 

911 log.debug("Looking for %s in %s", basefilename, current or os.getcwd()) 

912 jsonloader = cls.json_config_loader_class(basefilename + ".json", path=current, log=log) 

913 loaded: list[t.Any] = [] 

914 filenames: list[str] = [] 

915 for loader in [pyloader, jsonloader]: 

916 config = None 

917 try: 

918 config = loader.load_config() 

919 except ConfigFileNotFound: 

920 pass 

921 except Exception: 

922 # try to get the full filename, but it will be empty in the 

923 # unlikely event that the error raised before filefind finished 

924 filename = loader.full_filename or basefilename 

925 # problem while running the file 

926 if raise_config_file_errors: 

927 raise 

928 if log: 

929 log.error("Exception while loading config file %s", filename, exc_info=True) # noqa: G201 

930 else: 

931 if log: 

932 log.debug("Loaded config file: %s", loader.full_filename) 

933 if config: 

934 for filename, earlier_config in zip(filenames, loaded): 

935 collisions = earlier_config.collisions(config) 

936 if collisions and log: 

937 log.warning( 

938 "Collisions detected in {0} and {1} config files." # noqa: G001 

939 " {1} has higher priority: {2}".format( 

940 filename, 

941 loader.full_filename, 

942 json.dumps(collisions, indent=2), 

943 ) 

944 ) 

945 yield (config, loader.full_filename) 

946 loaded.append(config) 

947 filenames.append(loader.full_filename) 

948 

949 @property 

950 def loaded_config_files(self) -> list[str]: 

951 """Currently loaded configuration files""" 

952 return self._loaded_config_files[:] 

953 

954 @catch_config_error 

955 def load_config_file( 

956 self, filename: str, path: str | t.Sequence[str | None] | None = None 

957 ) -> None: 

958 """Load config files by filename and path.""" 

959 filename, ext = os.path.splitext(filename) 

960 new_config = Config() 

961 for config, fname in self._load_config_files( 

962 filename, 

963 path=path, 

964 log=self.log, 

965 raise_config_file_errors=self.raise_config_file_errors, 

966 ): 

967 new_config.merge(config) 

968 if ( 

969 fname not in self._loaded_config_files 

970 ): # only add to list of loaded files if not previously loaded 

971 self._loaded_config_files.append(fname) 

972 # add self.cli_config to preserve CLI config priority 

973 new_config.merge(self.cli_config) 

974 self.update_config(new_config) 

975 

976 @catch_config_error 

977 def load_config_environ(self) -> None: 

978 """Load config files by environment.""" 

979 PREFIX = self.name.upper().replace("-", "_") 

980 new_config = Config() 

981 

982 self.log.debug('Looping through config variables with prefix "%s"', PREFIX) 

983 

984 for k, v in os.environ.items(): 

985 if k.startswith(PREFIX): 

986 self.log.debug('Seeing environ "%s"="%s"', k, v) 

987 # use __ instead of . as separator in env variable. 

988 # Warning, case sensitive ! 

989 _, *path, key = k.split("__") 

990 section = new_config 

991 for p in path: 

992 section = section[p] 

993 setattr(section, key, DeferredConfigString(v)) 

994 

995 new_config.merge(self.cli_config) 

996 self.update_config(new_config) 

997 

998 def _classes_with_config_traits( 

999 self, classes: ClassesType | None = None 

1000 ) -> t.Generator[type[Configurable], None, None]: 

1001 """ 

1002 Yields only classes with configurable traits, and their subclasses. 

1003 

1004 :param classes: 

1005 The list of classes to iterate; if not set, uses :attr:`classes`. 

1006 

1007 Thus, produced sample config-file will contain all classes 

1008 on which a trait-value may be overridden: 

1009 

1010 - either on the class owning the trait, 

1011 - or on its subclasses, even if those subclasses do not define 

1012 any traits themselves. 

1013 """ 

1014 if classes is None: 

1015 classes = self.classes 

1016 

1017 cls_to_config = OrderedDict( 

1018 (cls, bool(cls.class_own_traits(config=True))) 

1019 for cls in self._classes_inc_parents(classes) 

1020 ) 

1021 

1022 def is_any_parent_included(cls: t.Any) -> bool: 

1023 return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__) 

1024 

1025 # Mark "empty" classes for inclusion if their parents own-traits, 

1026 # and loop until no more classes gets marked. 

1027 # 

1028 while True: 

1029 to_incl_orig = cls_to_config.copy() 

1030 cls_to_config = OrderedDict( 

1031 (cls, inc_yes or is_any_parent_included(cls)) 

1032 for cls, inc_yes in cls_to_config.items() 

1033 ) 

1034 if cls_to_config == to_incl_orig: 

1035 break 

1036 for cl, inc_yes in cls_to_config.items(): 

1037 if inc_yes: 

1038 yield cl 

1039 

1040 def generate_config_file(self, classes: ClassesType | None = None) -> str: 

1041 """generate default config file from Configurables""" 

1042 lines = ["# Configuration file for %s." % self.name] 

1043 lines.append("") 

1044 lines.append("c = get_config() #" + "noqa") 

1045 lines.append("") 

1046 classes = self.classes if classes is None else classes 

1047 config_classes = list(self._classes_with_config_traits(classes)) 

1048 for cls in config_classes: 

1049 lines.append(cls.class_config_section(config_classes)) 

1050 return "\n".join(lines) 

1051 

1052 def close_handlers(self) -> None: 

1053 if getattr(self, "_logging_configured", False): 

1054 # don't attempt to close handlers unless they have been opened 

1055 # (note accessing self.log.handlers will create handlers if they 

1056 # have not yet been initialised) 

1057 for handler in self.log.handlers: 

1058 with suppress(Exception): 

1059 handler.close() 

1060 self._logging_configured = False 

1061 

1062 def exit(self, exit_status: int | str | None = 0) -> None: 

1063 self.log.debug("Exiting application: %s", self.name) 

1064 self.close_handlers() 

1065 sys.exit(exit_status) 

1066 

1067 def __del__(self) -> None: 

1068 # __del__ may be called during process teardown, 

1069 # at which point any fraction of attributes and modules may have been cleared, 

1070 # e.g. even _accessing_ self.log may fail. 

1071 with suppress(Exception): 

1072 self.close_handlers() 

1073 

1074 @classmethod 

1075 def launch_instance(cls, argv: ArgvType = None, **kwargs: t.Any) -> None: 

1076 """Launch a global instance of this Application 

1077 

1078 If a global instance already exists, this reinitializes and starts it 

1079 """ 

1080 app = cls.instance(**kwargs) 

1081 app.initialize(argv) 

1082 app.start() 

1083 

1084 

1085# ----------------------------------------------------------------------------- 

1086# utility functions, for convenience 

1087# ----------------------------------------------------------------------------- 

1088 

1089default_aliases = Application.aliases 

1090default_flags = Application.flags 

1091 

1092 

1093def boolean_flag(name: str, configurable: str, set_help: str = "", unset_help: str = "") -> StrDict: 

1094 """Helper for building basic --trait, --no-trait flags. 

1095 

1096 Parameters 

1097 ---------- 

1098 name : str 

1099 The name of the flag. 

1100 configurable : str 

1101 The 'Class.trait' string of the trait to be set/unset with the flag 

1102 set_help : unicode 

1103 help string for --name flag 

1104 unset_help : unicode 

1105 help string for --no-name flag 

1106 

1107 Returns 

1108 ------- 

1109 cfg : dict 

1110 A dict with two keys: 'name', and 'no-name', for setting and unsetting 

1111 the trait, respectively. 

1112 """ 

1113 # default helpstrings 

1114 set_help = set_help or "set %s=True" % configurable 

1115 unset_help = unset_help or "set %s=False" % configurable 

1116 

1117 cls, trait = configurable.split(".") 

1118 

1119 setter = {cls: {trait: True}} 

1120 unsetter = {cls: {trait: False}} 

1121 return {name: (setter, set_help), "no-" + name: (unsetter, unset_help)} 

1122 

1123 

1124def get_config() -> Config: 

1125 """Get the config object for the global Application instance, if there is one 

1126 

1127 otherwise return an empty config object 

1128 """ 

1129 if Application.initialized(): 

1130 return Application.instance().config 

1131 else: 

1132 return Config() 

1133 

1134 

1135if __name__ == "__main__": 

1136 Application.launch_instance()