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

516 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

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", help="The date format used by logging formatters for %(asctime)s" 

219 ).tag(config=True) 

220 

221 log_format = Unicode( 

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

223 help="The Logging format template", 

224 ).tag(config=True) 

225 

226 def get_default_logging_config(self) -> StrDict: 

227 """Return the base logging configuration. 

228 

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

230 handler already exists. 

231 

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

233 by setting the ``log_level`` attribute. 

234 

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

236 control of logging. 

237 

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 } 

267 

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

274 

275 return config 

276 

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

284 

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

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

287 self._configure_logging() 

288 

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 

295 

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 _log = _log.parent # type:ignore[assignment] 

308 return log 

309 

310 logging_config = Dict( 

311 help=""" 

312 Configure additional log handlers. 

313 

314 The default stderr logs handler is configured by the 

315 log_level, log_datefmt and log_format settings. 

316 

317 This configuration can be used to configure additional handlers 

318 (e.g. to output the log to a file) or for finer control over the 

319 default handlers. 

320 

321 If provided this should be a logging configuration dictionary, for 

322 more information see: 

323 https://docs.python.org/3/library/logging.config.html#logging-config-dictschema 

324 

325 This dictionary is merged with the base logging configuration which 

326 defines the following: 

327 

328 * A logging formatter intended for interactive use called 

329 ``console``. 

330 * A logging handler that writes to stderr called 

331 ``console`` which uses the formatter ``console``. 

332 * A logger with the name of this application set to ``DEBUG`` 

333 level. 

334 

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

336 

337 .. code-block:: python 

338 

339 c.Application.logging_config = { 

340 "handlers": { 

341 "file": { 

342 "class": "logging.FileHandler", 

343 "level": "DEBUG", 

344 "filename": "<path/to/file>", 

345 } 

346 }, 

347 "loggers": { 

348 "<application-name>": { 

349 "level": "DEBUG", 

350 # NOTE: if you don't list the default "console" 

351 # handler here then it will be disabled 

352 "handlers": ["console", "file"], 

353 }, 

354 }, 

355 } 

356 

357 """, 

358 ).tag(config=True) 

359 

360 #: the alias map for configurables 

361 #: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`. 

362 #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text), 

363 # or just the "Class.trait" string, in which case the help text is inferred from the 

364 # corresponding trait 

365 aliases: StrDict = {"log-level": "Application.log_level"} 

366 

367 # flags for loading Configurables or store_const style flags 

368 # flags are loaded from this dict by '--key' flags 

369 # this must be a dict of two-tuples, the first element being the Config/dict 

370 # and the second being the help string for the flag 

371 flags: StrDict = { 

372 "debug": ( 

373 { 

374 "Application": { 

375 "log_level": logging.DEBUG, 

376 }, 

377 }, 

378 "Set log-level to debug, for the most verbose logging.", 

379 ), 

380 "show-config": ( 

381 { 

382 "Application": { 

383 "show_config": True, 

384 }, 

385 }, 

386 "Show the application's configuration (human-readable format)", 

387 ), 

388 "show-config-json": ( 

389 { 

390 "Application": { 

391 "show_config_json": True, 

392 }, 

393 }, 

394 "Show the application's configuration (json format)", 

395 ), 

396 } 

397 

398 # subcommands for launching other applications 

399 # if this is not empty, this will be a parent Application 

400 # this must be a dict of two-tuples, 

401 # the first element being the application class/import string 

402 # and the second being the help string for the subcommand 

403 subcommands: dict[str, t.Any] | Dict[str, t.Any] = Dict() 

404 # parse_command_line will initialize a subapp, if requested 

405 subapp = Instance("traitlets.config.application.Application", allow_none=True) 

406 

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

408 extra_args = List(Unicode()) 

409 

410 cli_config = Instance( 

411 Config, 

412 (), 

413 {}, 

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

415 

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

417 to ensure that it maintains highest priority. 

418 """, 

419 ) 

420 

421 _loaded_config_files: List[str] = List() 

422 

423 show_config = Bool( 

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

425 ).tag(config=True) 

426 

427 show_config_json = Bool( 

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

429 ).tag(config=True) 

430 

431 @observe("show_config_json") 

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

433 self.show_config = change.new 

434 

435 @observe("show_config") 

436 def _show_config_changed(self, change: Bunch) -> None: 

437 if change.new: 

438 self._save_start = self.start 

439 self.start = self.start_show_config # type:ignore[method-assign] 

440 

441 def __init__(self, **kwargs: t.Any) -> None: 

442 SingletonConfigurable.__init__(self, **kwargs) 

443 # Ensure my class is in self.classes, so my attributes appear in command line 

444 # options and config files. 

445 cls = self.__class__ 

446 if cls not in self.classes: 

447 if self.classes is cls.classes: 

448 # class attr, assign instead of insert 

449 self.classes = [cls, *self.classes] 

450 else: 

451 self.classes.insert(0, self.__class__) 

452 

453 @observe("config") 

454 @observe_compat 

455 def _config_changed(self, change: Bunch) -> None: 

456 super()._config_changed(change) 

457 self.log.debug("Config changed: %r", change.new) 

458 

459 @catch_config_error 

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

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

462 

463 Override in subclasses. 

464 """ 

465 self.parse_command_line(argv) 

466 

467 def start(self) -> None: 

468 """Start the app mainloop. 

469 

470 Override in subclasses. 

471 """ 

472 if self.subapp is not None: 

473 assert isinstance(self.subapp, Application) 

474 return self.subapp.start() 

475 

476 def start_show_config(self) -> None: 

477 """start function used when show_config is True""" 

478 config = self.config.copy() 

479 # exclude show_config flags from displayed config 

480 for cls in self.__class__.mro(): 

481 if cls.__name__ in config: 

482 cls_config = config[cls.__name__] 

483 cls_config.pop("show_config", None) 

484 cls_config.pop("show_config_json", None) 

485 

486 if self.show_config_json: 

487 json.dump(config, sys.stdout, indent=1, sort_keys=True, default=repr) 

488 # add trailing newline 

489 sys.stdout.write("\n") 

490 return 

491 

492 if self._loaded_config_files: 

493 print("Loaded config files:") 

494 for f in self._loaded_config_files: 

495 print(" " + f) 

496 print() 

497 

498 for classname in sorted(config): 

499 class_config = config[classname] 

500 if not class_config: 

501 continue 

502 print(classname) 

503 pformat_kwargs: StrDict = dict(indent=4, compact=True) # noqa: C408 

504 

505 for traitname in sorted(class_config): 

506 value = class_config[traitname] 

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

508 

509 def print_alias_help(self) -> None: 

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

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

512 

513 def emit_alias_help(self) -> t.Generator[str, None, None]: 

514 """Yield the lines for alias part of the help.""" 

515 if not self.aliases: 

516 return 

517 

518 classdict: dict[str, type[Configurable]] = {} 

519 for cls in self.classes: 

520 # include all parents (up to, but excluding Configurable) in available names 

521 for c in cls.mro()[:-3]: 

522 classdict[c.__name__] = t.cast(t.Type[Configurable], c) 

523 

524 fhelp: str | None 

525 for alias, longname in self.aliases.items(): 

526 try: 

527 if isinstance(longname, tuple): 

528 longname, fhelp = longname 

529 else: 

530 fhelp = None 

531 classname, traitname = longname.split(".")[-2:] 

532 longname = classname + "." + traitname 

533 cls = classdict[classname] 

534 

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

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

537 

538 if not isinstance(alias, tuple): # type:ignore[unreachable] 

539 alias = (alias,) # type:ignore[assignment] 

540 alias = sorted(alias, key=len) # type:ignore[assignment] 

541 alias = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in alias) 

542 

543 # reformat first line 

544 fhelp_lines[0] = fhelp_lines[0].replace("--" + longname, alias) 

545 yield from fhelp_lines 

546 yield indent("Equivalent to: [--%s]" % longname) 

547 except Exception as ex: 

548 self.log.error("Failed collecting help-message for alias %r, due to: %s", alias, ex) 

549 raise 

550 

551 def print_flag_help(self) -> None: 

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

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

554 

555 def emit_flag_help(self) -> t.Generator[str, None, None]: 

556 """Yield the lines for the flag part of the help.""" 

557 if not self.flags: 

558 return 

559 

560 for flags, (cfg, fhelp) in self.flags.items(): 

561 try: 

562 if not isinstance(flags, tuple): # type:ignore[unreachable] 

563 flags = (flags,) # type:ignore[assignment] 

564 flags = sorted(flags, key=len) # type:ignore[assignment] 

565 flags = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in flags) 

566 yield flags 

567 yield indent(dedent(fhelp.strip())) 

568 cfg_list = " ".join( 

569 f"--{clname}.{prop}={val}" 

570 for clname, props_dict in cfg.items() 

571 for prop, val in props_dict.items() 

572 ) 

573 cfg_txt = "Equivalent to: [%s]" % cfg_list 

574 yield indent(dedent(cfg_txt)) 

575 except Exception as ex: 

576 self.log.error("Failed collecting help-message for flag %r, due to: %s", flags, ex) 

577 raise 

578 

579 def print_options(self) -> None: 

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

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

582 

583 def emit_options_help(self) -> t.Generator[str, None, None]: 

584 """Yield the lines for the options part of the help.""" 

585 if not self.flags and not self.aliases: 

586 return 

587 header = "Options" 

588 yield header 

589 yield "=" * len(header) 

590 for p in wrap_paragraphs(self.option_description): 

591 yield p 

592 yield "" 

593 

594 yield from self.emit_flag_help() 

595 yield from self.emit_alias_help() 

596 yield "" 

597 

598 def print_subcommands(self) -> None: 

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

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

601 

602 def emit_subcommands_help(self) -> t.Generator[str, None, None]: 

603 """Yield the lines for the subcommand part of the help.""" 

604 if not self.subcommands: 

605 return 

606 

607 header = "Subcommands" 

608 yield header 

609 yield "=" * len(header) 

610 for p in wrap_paragraphs(self.subcommand_description.format(app=self.name)): 

611 yield p 

612 yield "" 

613 for subc, (_, help) in self.subcommands.items(): 

614 yield subc 

615 if help: 

616 yield indent(dedent(help.strip())) 

617 yield "" 

618 

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

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

621 

622 If classes=False (the default), print `--help-all` msg. 

623 """ 

624 if not classes: 

625 yield "To see all available configurables, use `--help-all`." 

626 yield "" 

627 

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

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

630 

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

632 """ 

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

634 

635 def emit_help(self, classes: bool = False) -> t.Generator[str, None, None]: 

636 """Yield the help-lines for each Configurable class in self.classes. 

637 

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

639 """ 

640 yield from self.emit_description() 

641 yield from self.emit_subcommands_help() 

642 yield from self.emit_options_help() 

643 

644 if classes: 

645 help_classes = self._classes_with_config_traits() 

646 if help_classes is not None: 

647 yield "Class options" 

648 yield "=============" 

649 for p in wrap_paragraphs(self.keyvalue_description): 

650 yield p 

651 yield "" 

652 

653 for cls in help_classes: 

654 yield cls.class_get_help() 

655 yield "" 

656 yield from self.emit_examples() 

657 

658 yield from self.emit_help_epilogue(classes) 

659 

660 def document_config_options(self) -> str: 

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

662 

663 Returns a multiline string. 

664 """ 

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

666 

667 def print_description(self) -> None: 

668 """Print the application description.""" 

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

670 

671 def emit_description(self) -> t.Generator[str, None, None]: 

672 """Yield lines with the application description.""" 

673 for p in wrap_paragraphs(self.description or self.__doc__ or ""): 

674 yield p 

675 yield "" 

676 

677 def print_examples(self) -> None: 

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

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

680 

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

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

683 

684 This usage string goes at the end of the command line help string 

685 and should contain examples of the application's usage. 

686 """ 

687 if self.examples: 

688 yield "Examples" 

689 yield "--------" 

690 yield "" 

691 yield indent(dedent(self.examples.strip())) 

692 yield "" 

693 

694 def print_version(self) -> None: 

695 """Print the version string.""" 

696 print(self.version) 

697 

698 @catch_config_error 

699 def initialize_subcommand(self, subc: str, argv: ArgvType = None) -> None: 

700 """Initialize a subcommand with argv.""" 

701 val = self.subcommands.get(subc) 

702 assert val is not None 

703 subapp, _ = val 

704 

705 if isinstance(subapp, str): 

706 subapp = import_item(subapp) 

707 

708 # Cannot issubclass() on a non-type (SOhttp://stackoverflow.com/questions/8692430) 

709 if isinstance(subapp, type) and issubclass(subapp, Application): 

710 # Clear existing instances before... 

711 self.__class__.clear_instance() 

712 # instantiating subapp... 

713 self.subapp = subapp.instance(parent=self) 

714 elif callable(subapp): 

715 # or ask factory to create it... 

716 self.subapp = subapp(self) 

717 else: 

718 raise AssertionError("Invalid mappings for subcommand '%s'!" % subc) 

719 

720 # ... and finally initialize subapp. 

721 self.subapp.initialize(argv) 

722 

723 def flatten_flags(self) -> tuple[dict[str, t.Any], dict[str, t.Any]]: 

724 """Flatten flags and aliases for loaders, so cl-args override as expected. 

725 

726 This prevents issues such as an alias pointing to InteractiveShell, 

727 but a config file setting the same trait in TerminalInteraciveShell 

728 getting inappropriate priority over the command-line arg. 

729 Also, loaders expect ``(key: longname)`` and not ``key: (longname, help)`` items. 

730 

731 Only aliases with exactly one descendent in the class list 

732 will be promoted. 

733 

734 """ 

735 # build a tree of classes in our list that inherit from a particular 

736 # it will be a dict by parent classname of classes in our list 

737 # that are descendents 

738 mro_tree = defaultdict(list) 

739 for cls in self.classes: 

740 clsname = cls.__name__ 

741 for parent in cls.mro()[1:-3]: 

742 # exclude cls itself and Configurable,HasTraits,object 

743 mro_tree[parent.__name__].append(clsname) 

744 # flatten aliases, which have the form: 

745 # { 'alias' : 'Class.trait' } 

746 aliases: dict[str, str] = {} 

747 for alias, longname in self.aliases.items(): 

748 if isinstance(longname, tuple): 

749 longname, _ = longname 

750 cls, trait = longname.split(".", 1) 

751 children = mro_tree[cls] # type:ignore[index] 

752 if len(children) == 1: 

753 # exactly one descendent, promote alias 

754 cls = children[0] # type:ignore[assignment] 

755 if not isinstance(aliases, tuple): # type:ignore[unreachable] 

756 alias = (alias,) # type:ignore[assignment] 

757 for al in alias: 

758 aliases[al] = ".".join([cls, trait]) # type:ignore[list-item] 

759 

760 # flatten flags, which are of the form: 

761 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')} 

762 flags = {} 

763 for key, (flagdict, help) in self.flags.items(): 

764 newflag: dict[t.Any, t.Any] = {} 

765 for cls, subdict in flagdict.items(): 

766 children = mro_tree[cls] # type:ignore[index] 

767 # exactly one descendent, promote flag section 

768 if len(children) == 1: 

769 cls = children[0] # type:ignore[assignment] 

770 

771 if cls in newflag: 

772 newflag[cls].update(subdict) 

773 else: 

774 newflag[cls] = subdict 

775 

776 if not isinstance(key, tuple): # type:ignore[unreachable] 

777 key = (key,) # type:ignore[assignment] 

778 for k in key: 

779 flags[k] = (newflag, help) 

780 return flags, aliases 

781 

782 def _create_loader( 

783 self, 

784 argv: list[str] | None, 

785 aliases: StrDict, 

786 flags: StrDict, 

787 classes: ClassesType | None, 

788 ) -> KVArgParseConfigLoader: 

789 return KVArgParseConfigLoader( 

790 argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands 

791 ) 

792 

793 @classmethod 

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

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

796 

797 `argcomplete`'s strategy is to call the python script with no arguments, 

798 so ``len(sys.argv) == 1``, and run until the `ArgumentParser` is constructed 

799 and determine what completions are available. 

800 

801 On the other hand, `traitlet`'s subcommand-handling strategy is to check 

802 ``sys.argv[1]`` and see if it matches a subcommand, and if so then dynamically 

803 load the subcommand app and initialize it with ``sys.argv[1:]``. 

804 

805 This helper method helps to take the current tokens for `argcomplete` and pass 

806 them through as `argv`. 

807 """ 

808 if check_argcomplete and "_ARGCOMPLETE" in os.environ: 

809 try: 

810 from traitlets.config.argcomplete_config import get_argcomplete_cwords 

811 

812 cwords = get_argcomplete_cwords() 

813 assert cwords is not None 

814 return cwords 

815 except (ImportError, ModuleNotFoundError): 

816 pass 

817 return sys.argv 

818 

819 @classmethod 

820 def _handle_argcomplete_for_subcommand(cls) -> None: 

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

822 

823 `argcomplete` does not know that `traitlets` has already consumed subcommands, 

824 as it only "sees" the final `argparse.ArgumentParser` that is constructed. 

825 (Indeed `KVArgParseConfigLoader` does not get passed subcommands at all currently.) 

826 We explicitly manipulate the environment variables used internally by `argcomplete` 

827 to get it to skip over the subcommand tokens. 

828 """ 

829 if "_ARGCOMPLETE" not in os.environ: 

830 return 

831 

832 try: 

833 from traitlets.config.argcomplete_config import increment_argcomplete_index 

834 

835 increment_argcomplete_index() 

836 except (ImportError, ModuleNotFoundError): 

837 pass 

838 

839 @catch_config_error 

840 def parse_command_line(self, argv: ArgvType = None) -> None: 

841 """Parse the command line arguments.""" 

842 assert not isinstance(argv, str) 

843 if argv is None: 

844 argv = self._get_sys_argv(check_argcomplete=bool(self.subcommands))[1:] 

845 self.argv = [cast_unicode(arg) for arg in argv] 

846 

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

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

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

850 

851 if self.subcommands and len(argv) > 0: 

852 # we have subcommands, and one may have been specified 

853 subc, subargv = argv[0], argv[1:] 

854 if re.match(r"^\w(\-?\w)*$", subc) and subc in self.subcommands: 

855 # it's a subcommand, and *not* a flag or class parameter 

856 self._handle_argcomplete_for_subcommand() 

857 return self.initialize_subcommand(subc, subargv) 

858 

859 # Arguments after a '--' argument are for the script IPython may be 

860 # about to run, not IPython iteslf. For arguments parsed here (help and 

861 # version), we want to only search the arguments up to the first 

862 # occurrence of '--', which we're calling interpreted_argv. 

863 try: 

864 interpreted_argv = argv[: argv.index("--")] 

865 except ValueError: 

866 interpreted_argv = argv 

867 

868 if any(x in interpreted_argv for x in ("-h", "--help-all", "--help")): 

869 self.print_help("--help-all" in interpreted_argv) 

870 self.exit(0) 

871 

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

873 self.print_version() 

874 self.exit(0) 

875 

876 # flatten flags&aliases, so cl-args get appropriate priority: 

877 flags, aliases = self.flatten_flags() 

878 classes = list(self._classes_with_config_traits()) 

879 loader = self._create_loader(argv, aliases, flags, classes=classes) 

880 try: 

881 self.cli_config = deepcopy(loader.load_config()) 

882 except SystemExit: 

883 # traitlets 5: no longer print help output on error 

884 # help output is huge, and comes after the error 

885 raise 

886 self.update_config(self.cli_config) 

887 # store unparsed args in extra_args 

888 self.extra_args = loader.extra_args 

889 

890 @classmethod 

891 def _load_config_files( 

892 cls, 

893 basefilename: str, 

894 path: str | t.Sequence[str | None] | None, 

895 log: AnyLogger | None = None, 

896 raise_config_file_errors: bool = False, 

897 ) -> t.Generator[t.Any, None, None]: 

898 """Load config files (py,json) by filename and path. 

899 

900 yield each config object in turn. 

901 """ 

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

903 path = [path] 

904 for current in reversed(path): 

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

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

907 if log: 

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

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

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

911 filenames: list[str] = [] 

912 for loader in [pyloader, jsonloader]: 

913 config = None 

914 try: 

915 config = loader.load_config() 

916 except ConfigFileNotFound: 

917 pass 

918 except Exception: 

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

920 # unlikely event that the error raised before filefind finished 

921 filename = loader.full_filename or basefilename 

922 # problem while running the file 

923 if raise_config_file_errors: 

924 raise 

925 if log: 

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

927 else: 

928 if log: 

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

930 if config: 

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

932 collisions = earlier_config.collisions(config) 

933 if collisions and log: 

934 log.warning( 

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

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

937 filename, 

938 loader.full_filename, 

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

940 ) 

941 ) 

942 yield (config, loader.full_filename) 

943 loaded.append(config) 

944 filenames.append(loader.full_filename) 

945 

946 @property 

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

948 """Currently loaded configuration files""" 

949 return self._loaded_config_files[:] 

950 

951 @catch_config_error 

952 def load_config_file( 

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

954 ) -> None: 

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

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

957 new_config = Config() 

958 for config, fname in self._load_config_files( 

959 filename, 

960 path=path, 

961 log=self.log, 

962 raise_config_file_errors=self.raise_config_file_errors, 

963 ): 

964 new_config.merge(config) 

965 if ( 

966 fname not in self._loaded_config_files 

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

968 self._loaded_config_files.append(fname) 

969 # add self.cli_config to preserve CLI config priority 

970 new_config.merge(self.cli_config) 

971 self.update_config(new_config) 

972 

973 @catch_config_error 

974 def load_config_environ(self) -> None: 

975 """Load config files by environment.""" 

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

977 new_config = Config() 

978 

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

980 

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

982 if k.startswith(PREFIX): 

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

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

985 # Warning, case sensitive ! 

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

987 section = new_config 

988 for p in path: 

989 section = section[p] 

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

991 

992 new_config.merge(self.cli_config) 

993 self.update_config(new_config) 

994 

995 def _classes_with_config_traits( 

996 self, classes: ClassesType | None = None 

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

998 """ 

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

1000 

1001 :param classes: 

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

1003 

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

1005 on which a trait-value may be overridden: 

1006 

1007 - either on the class owning the trait, 

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

1009 any traits themselves. 

1010 """ 

1011 if classes is None: 

1012 classes = self.classes 

1013 

1014 cls_to_config = OrderedDict( 

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

1016 for cls in self._classes_inc_parents(classes) 

1017 ) 

1018 

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

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

1021 

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

1023 # and loop until no more classes gets marked. 

1024 # 

1025 while True: 

1026 to_incl_orig = cls_to_config.copy() 

1027 cls_to_config = OrderedDict( 

1028 (cls, inc_yes or is_any_parent_included(cls)) 

1029 for cls, inc_yes in cls_to_config.items() 

1030 ) 

1031 if cls_to_config == to_incl_orig: 

1032 break 

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

1034 if inc_yes: 

1035 yield cl 

1036 

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

1038 """generate default config file from Configurables""" 

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

1040 lines.append("") 

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

1042 lines.append("") 

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

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

1045 for cls in config_classes: 

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

1047 return "\n".join(lines) 

1048 

1049 def close_handlers(self) -> None: 

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

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

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

1053 # have not yet been initialised) 

1054 for handler in self.log.handlers: 

1055 with suppress(Exception): 

1056 handler.close() 

1057 self._logging_configured = False 

1058 

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

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

1061 self.close_handlers() 

1062 sys.exit(exit_status) 

1063 

1064 def __del__(self) -> None: 

1065 self.close_handlers() 

1066 

1067 @classmethod 

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

1069 """Launch a global instance of this Application 

1070 

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

1072 """ 

1073 app = cls.instance(**kwargs) 

1074 app.initialize(argv) 

1075 app.start() 

1076 

1077 

1078# ----------------------------------------------------------------------------- 

1079# utility functions, for convenience 

1080# ----------------------------------------------------------------------------- 

1081 

1082default_aliases = Application.aliases 

1083default_flags = Application.flags 

1084 

1085 

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

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

1088 

1089 Parameters 

1090 ---------- 

1091 name : str 

1092 The name of the flag. 

1093 configurable : str 

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

1095 set_help : unicode 

1096 help string for --name flag 

1097 unset_help : unicode 

1098 help string for --no-name flag 

1099 

1100 Returns 

1101 ------- 

1102 cfg : dict 

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

1104 the trait, respectively. 

1105 """ 

1106 # default helpstrings 

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

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

1109 

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

1111 

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

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

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

1115 

1116 

1117def get_config() -> Config: 

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

1119 

1120 otherwise return an empty config object 

1121 """ 

1122 if Application.initialized(): 

1123 return Application.instance().config 

1124 else: 

1125 return Config() 

1126 

1127 

1128if __name__ == "__main__": 

1129 Application.launch_instance()