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

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: https://docs.python.org/3/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 f"Unsupported value for environment variable: 'TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR' is set to '{_envvar}' which is none of {{'0', '1', 'false', 'true', ''}}." 

93 ) 

94 

95 

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

97 

98T = t.TypeVar("T", bound=t.Callable[..., t.Any]) 

99AnyLogger = t.Union[logging.Logger, "logging.LoggerAdapter[t.Any]"] 

100StrDict = dict[str, t.Any] 

101ArgvType = list[str] | None 

102ClassesType = list[type[Configurable]] 

103 

104 

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

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

107 

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

109 message, and exit the app. 

110 

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

112 """ 

113 

114 @functools.wraps(method) 

115 def inner(app: Application, *args: t.Any, **kwargs: t.Any) -> t.Any: 

116 try: 

117 return method(app, *args, **kwargs) 

118 except (TraitError, ArgumentError) as e: 

119 app.log.fatal("Bad config encountered during initialization: %s", e) 

120 app.log.debug("Config at the time: %s", app.config) 

121 app.exit(1) 

122 

123 return t.cast(T, inner) 

124 

125 

126class ApplicationError(Exception): 

127 pass 

128 

129 

130class LevelFormatter(logging.Formatter): 

131 """Formatter with additional `highlevel` record 

132 

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

134 otherwise it is formatted with self.highlevel_format. 

135 

136 Useful for adding 'WARNING' to warning messages, 

137 without adding 'INFO' to info, etc. 

138 """ 

139 

140 highlevel_limit = logging.WARN 

141 highlevel_format = " %(levelname)s |" 

142 

143 def format(self, record: logging.LogRecord) -> str: 

144 if record.levelno >= self.highlevel_limit: 

145 record.highlevel = self.highlevel_format % record.__dict__ 

146 else: 

147 record.highlevel = "" 

148 return super().format(record) 

149 

150 

151class Application(SingletonConfigurable): 

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

153 

154 # The name of the application, will usually match the name of the command 

155 # line application 

156 name: str | Unicode[str, str | bytes] = Unicode("application") 

157 

158 # The description of the application that is printed at the beginning 

159 # of the help. 

160 description: str | Unicode[str, str | bytes] = Unicode("This is an application.") 

161 # default section descriptions 

162 option_description: str | Unicode[str, str | bytes] = Unicode(option_description) 

163 keyvalue_description: str | Unicode[str, str | bytes] = Unicode(keyvalue_description) 

164 subcommand_description: str | Unicode[str, str | bytes] = Unicode(subcommand_description) 

165 

166 python_config_loader_class = PyFileConfigLoader 

167 json_config_loader_class = JSONFileConfigLoader 

168 

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

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

171 

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

173 # be exposed at the command line. 

174 classes: ClassesType = [] 

175 

176 def _classes_inc_parents( 

177 self, classes: ClassesType | None = None 

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

179 """Iterate through configurable classes, including configurable parents 

180 

181 :param classes: 

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

183 

184 Children should always be after parents, and each class should only be 

185 yielded once. 

186 """ 

187 if classes is None: 

188 classes = self.classes 

189 

190 seen = set() 

191 for c in classes: 

192 # We want to sort parents before children, so we reverse the MRO 

193 for parent in reversed(c.mro()): 

194 if issubclass(parent, Configurable) and (parent not in seen): 

195 seen.add(parent) 

196 yield parent 

197 

198 # The version string of this application. 

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

200 

201 # the argv used to initialize the application 

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

203 

204 # Whether failing to load config files should prevent startup 

205 raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR) 

206 

207 # The log level for the application 

208 log_level = Enum( 

209 (0, 10, 20, 30, 40, 50, "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"), 

210 default_value=logging.WARN, 

211 help="Set the log level by value or name.", 

212 ).tag(config=True) 

213 

214 _log_formatter_cls = LevelFormatter 

215 

216 log_datefmt = Unicode( 

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

218 help="The date format used by logging formatters for `logging.Formatter` ``datefmt`` parameter", 

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[call-overload] 

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

247 }, 

248 }, 

249 "formatters": { 

250 "console": { 

251 "class": ( 

252 f"{self._log_formatter_cls.__module__}.{self._log_formatter_cls.__name__}" 

253 ), 

254 "format": self.log_format, 

255 "datefmt": self.log_datefmt, 

256 }, 

257 }, 

258 "loggers": { 

259 self.__class__.__name__: { 

260 "level": "DEBUG", 

261 "handlers": ["console"], 

262 } 

263 }, 

264 "disable_existing_loggers": False, 

265 } 

266 

267 if IS_PYTHONW: 

268 # disable logging 

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

270 # hooked up in parallel applications) 

271 del config["handlers"] 

272 del config["loggers"] 

273 

274 return config 

275 

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

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

278 # convert log level strings to ints 

279 log_level = self.log_level 

280 if isinstance(log_level, str): 

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

282 self._configure_logging() 

283 

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

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

286 self._configure_logging() 

287 

288 def _configure_logging(self) -> None: 

289 config = self.get_default_logging_config() 

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

291 dictConfig(config) 

292 # make a note that we have configured logging 

293 self._logging_configured = True 

294 

295 @default("log") 

296 def _log_default(self) -> AnyLogger: 

297 """Start logging for this application.""" 

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

299 log.propagate = False 

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

301 while _log is not None: 

302 if _log.handlers: 

303 return log 

304 if not _log.propagate: 

305 break 

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

307 return log 

308 

309 logging_config = Dict( 

310 help=""" 

311 Configure additional log handlers. 

312 

313 The default stderr logs handler is configured by the 

314 log_level, log_datefmt and log_format settings. 

315 

316 This configuration can be used to configure additional handlers 

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

318 default handlers. 

319 

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

321 more information see: 

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

323 

324 This dictionary is merged with the base logging configuration which 

325 defines the following: 

326 

327 * A logging formatter intended for interactive use called 

328 ``console``. 

329 * A logging handler that writes to stderr called 

330 ``console`` which uses the formatter ``console``. 

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

332 level. 

333 

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

335 

336 .. code-block:: python 

337 

338 c.Application.logging_config = { 

339 "handlers": { 

340 "file": { 

341 "class": "logging.FileHandler", 

342 "level": "DEBUG", 

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

344 } 

345 }, 

346 "loggers": { 

347 "<application-name>": { 

348 "level": "DEBUG", 

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

350 # handler here then it will be disabled 

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

352 }, 

353 }, 

354 } 

355 

356 """, 

357 ).tag(config=True) 

358 

359 #: the alias map for configurables 

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

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

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

363 # corresponding trait 

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

365 

366 # flags for loading Configurables or store_const style flags 

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

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

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

370 flags: StrDict = { 

371 "debug": ( 

372 { 

373 "Application": { 

374 "log_level": logging.DEBUG, 

375 }, 

376 }, 

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

378 ), 

379 "show-config": ( 

380 { 

381 "Application": { 

382 "show_config": True, 

383 }, 

384 }, 

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

386 ), 

387 "show-config-json": ( 

388 { 

389 "Application": { 

390 "show_config_json": True, 

391 }, 

392 }, 

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

394 ), 

395 } 

396 

397 # subcommands for launching other applications 

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

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

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

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

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

403 # parse_command_line will initialize a subapp, if requested 

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

405 

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

407 extra_args = List(Unicode()) 

408 

409 cli_config = Instance( 

410 Config, 

411 (), 

412 {}, 

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

414 

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

416 to ensure that it maintains highest priority. 

417 """, 

418 ) 

419 

420 _loaded_config_files: List[str] = List() 

421 

422 show_config = Bool( 

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

424 ).tag(config=True) 

425 

426 show_config_json = Bool( 

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

428 ).tag(config=True) 

429 

430 @observe("show_config_json") 

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

432 self.show_config = change.new 

433 

434 @observe("show_config") 

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

436 if change.new: 

437 self._save_start = self.start 

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

439 

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

441 SingletonConfigurable.__init__(self, **kwargs) 

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

443 # options and config files. 

444 cls = self.__class__ 

445 if cls not in self.classes: 

446 if self.classes is cls.classes: 

447 # class attr, assign instead of insert 

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

449 else: 

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

451 

452 @observe("config") 

453 @observe_compat 

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

455 super()._config_changed(change) 

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

457 

458 @catch_config_error 

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

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

461 

462 Override in subclasses. 

463 """ 

464 self.parse_command_line(argv) 

465 

466 def start(self) -> None: 

467 """Start the app mainloop. 

468 

469 Override in subclasses. 

470 """ 

471 if self.subapp is not None: 

472 assert isinstance(self.subapp, Application) 

473 return self.subapp.start() 

474 

475 def start_show_config(self) -> None: 

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

477 config = self.config.copy() 

478 # exclude show_config flags from displayed config 

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

480 if cls.__name__ in config: 

481 cls_config = config[cls.__name__] 

482 cls_config.pop("show_config", None) 

483 cls_config.pop("show_config_json", None) 

484 

485 if self.show_config_json: 

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

487 # add trailing newline 

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

489 return 

490 

491 if self._loaded_config_files: 

492 print("Loaded config files:") 

493 for f in self._loaded_config_files: 

494 print(" " + f) 

495 print() 

496 

497 for classname in sorted(config): 

498 class_config = config[classname] 

499 if not class_config: 

500 continue 

501 print(classname) 

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

503 

504 for traitname in sorted(class_config): 

505 value = class_config[traitname] 

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

507 

508 def print_alias_help(self) -> None: 

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

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

511 

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

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

514 if not self.aliases: 

515 return 

516 

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

518 for cls in self.classes: 

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

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

521 classdict[c.__name__] = t.cast(type[Configurable], c) 

522 

523 fhelp: str | None 

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

525 try: 

526 if isinstance(longname, tuple): 

527 longname, fhelp = longname 

528 else: 

529 fhelp = None 

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

531 longname = classname + "." + traitname 

532 cls = classdict[classname] 

533 

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

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

536 

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

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

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

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

541 

542 # reformat first line 

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

544 yield from fhelp_lines 

545 yield indent(f"Equivalent to: [--{longname}]") 

546 except Exception as ex: 

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

548 raise 

549 

550 def print_flag_help(self) -> None: 

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

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

553 

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

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

556 if not self.flags: 

557 return 

558 

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

560 try: 

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

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

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

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

565 yield flags 

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

567 cfg_list = " ".join( 

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

569 for clname, props_dict in cfg.items() 

570 for prop, val in props_dict.items() 

571 ) 

572 cfg_txt = f"Equivalent to: [{cfg_list}]" 

573 yield indent(dedent(cfg_txt)) 

574 except Exception as ex: 

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

576 raise 

577 

578 def print_options(self) -> None: 

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

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

581 

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

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

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

585 return 

586 header = "Options" 

587 yield header 

588 yield "=" * len(header) 

589 for p in wrap_paragraphs(self.option_description): 

590 yield p 

591 yield "" 

592 

593 yield from self.emit_flag_help() 

594 yield from self.emit_alias_help() 

595 yield "" 

596 

597 def print_subcommands(self) -> None: 

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

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

600 

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

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

603 if not self.subcommands: 

604 return 

605 

606 header = "Subcommands" 

607 yield header 

608 yield "=" * len(header) 

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

610 yield p 

611 yield "" 

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

613 yield subc 

614 if help: 

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

616 yield "" 

617 

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

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

620 

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

622 """ 

623 if not classes: 

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

625 yield "" 

626 

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

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

629 

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

631 """ 

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

633 

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

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

636 

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

638 """ 

639 yield from self.emit_description() 

640 yield from self.emit_subcommands_help() 

641 yield from self.emit_options_help() 

642 

643 if classes: 

644 help_classes = self._classes_with_config_traits() 

645 if help_classes is not None: 

646 yield "Class options" 

647 yield "=============" 

648 for p in wrap_paragraphs(self.keyvalue_description): 

649 yield p 

650 yield "" 

651 

652 for cls in help_classes: 

653 yield cls.class_get_help() 

654 yield "" 

655 yield from self.emit_examples() 

656 

657 yield from self.emit_help_epilogue(classes) 

658 

659 def document_config_options(self) -> str: 

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

661 

662 Returns a multiline string. 

663 """ 

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

665 

666 def print_description(self) -> None: 

667 """Print the application description.""" 

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

669 

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

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

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

673 yield p 

674 yield "" 

675 

676 def print_examples(self) -> None: 

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

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

679 

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

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

682 

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

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

685 """ 

686 if self.examples: 

687 yield "Examples" 

688 yield "--------" 

689 yield "" 

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

691 yield "" 

692 

693 def print_version(self) -> None: 

694 """Print the version string.""" 

695 print(self.version) 

696 

697 @catch_config_error 

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

699 """Initialize a subcommand with argv.""" 

700 val = self.subcommands.get(subc) 

701 assert val is not None 

702 subapp, _ = val 

703 

704 if isinstance(subapp, str): 

705 subapp = import_item(subapp) 

706 

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

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

709 # Clear existing instances before... 

710 self.__class__.clear_instance() 

711 # instantiating subapp... 

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

713 elif callable(subapp): 

714 # or ask factory to create it... 

715 self.subapp = subapp(self) 

716 else: 

717 raise AssertionError(f"Invalid mappings for subcommand '{subc}'!") 

718 

719 # ... and finally initialize subapp. 

720 self.subapp.initialize(argv) 

721 

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

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

724 

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

726 but a config file setting the same trait in TerminalInteraciveShell 

727 getting inappropriate priority over the command-line arg. 

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

729 

730 Only aliases with exactly one descendent in the class list 

731 will be promoted. 

732 

733 """ 

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

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

736 # that are descendents 

737 mro_tree = defaultdict(list) 

738 for cls in self.classes: 

739 clsname = cls.__name__ 

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

741 # exclude cls itself and Configurable,HasTraits,object 

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

743 # flatten aliases, which have the form: 

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

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

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

747 if isinstance(longname, tuple): 

748 longname, _ = longname 

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

750 children = mro_tree[cls] 

751 if len(children) == 1: 

752 # exactly one descendent, promote alias 

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

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

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

756 for al in alias: 

757 aliases[al] = ".".join([cls, trait]) 

758 

759 # flatten flags, which are of the form: 

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

761 flags = {} 

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

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

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

765 children = mro_tree[cls] 

766 # exactly one descendent, promote flag section 

767 if len(children) == 1: 

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

769 

770 if cls in newflag: 

771 newflag[cls].update(subdict) 

772 else: 

773 newflag[cls] = subdict 

774 

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

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

777 for k in key: 

778 flags[k] = (newflag, help) 

779 return flags, aliases 

780 

781 def _create_loader( 

782 self, 

783 argv: list[str] | None, 

784 aliases: StrDict, 

785 flags: StrDict, 

786 classes: ClassesType | None, 

787 ) -> KVArgParseConfigLoader: 

788 return KVArgParseConfigLoader( 

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

790 ) 

791 

792 @classmethod 

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

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

795 

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

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

798 and determine what completions are available. 

799 

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

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

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

803 

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

805 them through as `argv`. 

806 """ 

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

808 try: 

809 from traitlets.config.argcomplete_config import get_argcomplete_cwords 

810 

811 cwords = get_argcomplete_cwords() 

812 assert cwords is not None 

813 return cwords 

814 except (ImportError, ModuleNotFoundError): 

815 pass 

816 return sys.argv 

817 

818 @classmethod 

819 def _handle_argcomplete_for_subcommand(cls) -> None: 

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

821 

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

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

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

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

826 to get it to skip over the subcommand tokens. 

827 """ 

828 if "_ARGCOMPLETE" not in os.environ: 

829 return 

830 

831 try: 

832 from traitlets.config.argcomplete_config import increment_argcomplete_index 

833 

834 increment_argcomplete_index() 

835 except (ImportError, ModuleNotFoundError): 

836 pass 

837 

838 @catch_config_error 

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

840 """Parse the command line arguments.""" 

841 assert not isinstance(argv, str) 

842 if argv is None: 

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

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

845 

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

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

848 argv = [*argv[1:], "-h"] 

849 

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

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

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

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

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

855 self._handle_argcomplete_for_subcommand() 

856 return self.initialize_subcommand(subc, subargv) 

857 

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

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

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

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

862 try: 

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

864 except ValueError: 

865 interpreted_argv = argv 

866 

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

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

869 self.exit(0) 

870 

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

872 self.print_version() 

873 self.exit(0) 

874 

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

876 flags, aliases = self.flatten_flags() 

877 classes = list(self._classes_with_config_traits()) 

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

879 try: 

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

881 except SystemExit: 

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

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

884 raise 

885 self.update_config(self.cli_config) 

886 # store unparsed args in extra_args 

887 self.extra_args = loader.extra_args 

888 

889 @classmethod 

890 def _load_config_files( 

891 cls, 

892 basefilename: str, 

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

894 log: AnyLogger | None = None, 

895 raise_config_file_errors: bool = False, 

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

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

898 

899 yield each config object in turn. 

900 """ 

901 if os.path.isabs(basefilename): 

902 path = [None] 

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

904 path = [path] 

905 for current in reversed(path): 

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

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

908 if log: 

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

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

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

912 filenames: list[str] = [] 

913 for loader in [pyloader, jsonloader]: 

914 config = None 

915 try: 

916 config = loader.load_config() 

917 except ConfigFileNotFound: 

918 pass 

919 except Exception: 

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

921 # unlikely event that the error raised before filefind finished 

922 filename = loader.full_filename or basefilename 

923 # problem while running the file 

924 if raise_config_file_errors: 

925 raise 

926 if log: 

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

928 else: 

929 if log: 

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

931 if config: 

932 for filename, earlier_config in zip(filenames, loaded, strict=True): 

933 collisions = earlier_config.collisions(config) 

934 if collisions and log: 

935 log.warning( 

936 "Collisions detected in %s and %s config files." 

937 " %s has higher priority: %s", 

938 filename, 

939 loader.full_filename, 

940 loader.full_filename, 

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

942 ) 

943 yield (config, loader.full_filename) 

944 loaded.append(config) 

945 filenames.append(loader.full_filename) 

946 

947 @property 

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

949 """Currently loaded configuration files""" 

950 return self._loaded_config_files[:] 

951 

952 @catch_config_error 

953 def load_config_file( 

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

955 ) -> None: 

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

957 filename, _ext = os.path.splitext(filename) 

958 new_config = Config() 

959 for config, fname in self._load_config_files( 

960 filename, 

961 path=path, 

962 log=self.log, 

963 raise_config_file_errors=self.raise_config_file_errors, 

964 ): 

965 new_config.merge(config) 

966 if ( 

967 fname not in self._loaded_config_files 

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

969 self._loaded_config_files.append(fname) 

970 # add self.cli_config to preserve CLI config priority 

971 new_config.merge(self.cli_config) 

972 self.update_config(new_config) 

973 

974 @catch_config_error 

975 def load_config_environ(self) -> None: 

976 """Load config files by environment.""" 

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

978 new_config = Config() 

979 

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

981 

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

983 if k.startswith(PREFIX): 

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

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

986 # Warning, case sensitive ! 

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

988 section = new_config 

989 for p in path: 

990 section = section[p] 

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

992 

993 new_config.merge(self.cli_config) 

994 self.update_config(new_config) 

995 

996 def _classes_with_config_traits( 

997 self, classes: ClassesType | None = None 

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

999 """ 

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

1001 

1002 :param classes: 

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

1004 

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

1006 on which a trait-value may be overridden: 

1007 

1008 - either on the class owning the trait, 

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

1010 any traits themselves. 

1011 """ 

1012 if classes is None: 

1013 classes = self.classes 

1014 

1015 cls_to_config = OrderedDict( 

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

1017 for cls in self._classes_inc_parents(classes) 

1018 ) 

1019 

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

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

1022 

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

1024 # and loop until no more classes gets marked. 

1025 # 

1026 while True: 

1027 to_incl_orig = cls_to_config.copy() 

1028 cls_to_config = OrderedDict( 

1029 (cls, inc_yes or is_any_parent_included(cls)) 

1030 for cls, inc_yes in cls_to_config.items() 

1031 ) 

1032 if cls_to_config == to_incl_orig: 

1033 break 

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

1035 if inc_yes: 

1036 yield cl 

1037 

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

1039 """generate default config file from Configurables""" 

1040 lines = [f"# Configuration file for {self.name}."] 

1041 lines.append("") 

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

1043 lines.append("") 

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

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

1046 for cls in config_classes: 

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

1048 return "\n".join(lines) 

1049 

1050 def close_handlers(self) -> None: 

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

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

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

1054 # have not yet been initialised) 

1055 for handler in self.log.handlers: 

1056 with suppress(Exception): 

1057 handler.close() 

1058 self._logging_configured = False 

1059 

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

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

1062 self.close_handlers() 

1063 sys.exit(exit_status) 

1064 

1065 def __del__(self) -> None: 

1066 # __del__ may be called during process teardown, 

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

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

1069 with suppress(Exception): 

1070 self.close_handlers() 

1071 

1072 @classmethod 

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

1074 """Launch a global instance of this Application 

1075 

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

1077 """ 

1078 app = cls.instance(**kwargs) 

1079 app.initialize(argv) 

1080 app.start() 

1081 

1082 

1083# ----------------------------------------------------------------------------- 

1084# utility functions, for convenience 

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

1086 

1087default_aliases = Application.aliases 

1088default_flags = Application.flags 

1089 

1090 

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

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

1093 

1094 Parameters 

1095 ---------- 

1096 name : str 

1097 The name of the flag. 

1098 configurable : str 

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

1100 set_help : unicode 

1101 help string for --name flag 

1102 unset_help : unicode 

1103 help string for --no-name flag 

1104 

1105 Returns 

1106 ------- 

1107 cfg : dict 

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

1109 the trait, respectively. 

1110 """ 

1111 # default helpstrings 

1112 set_help = set_help or f"set {configurable}=True" 

1113 unset_help = unset_help or f"set {configurable}=False" 

1114 

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

1116 

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

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

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

1120 

1121 

1122def get_config() -> Config: 

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

1124 

1125 otherwise return an empty config object 

1126 """ 

1127 if Application.initialized(): 

1128 return Application.instance().config 

1129 else: 

1130 return Config() 

1131 

1132 

1133if __name__ == "__main__": 

1134 Application.launch_instance()