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

496 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +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. 

5 

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 

20from typing import Any, Callable, TypeVar, cast 

21 

22from traitlets.config.configurable import Configurable, SingletonConfigurable 

23from traitlets.config.loader import ( 

24 ArgumentError, 

25 Config, 

26 ConfigFileNotFound, 

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.nested_update import nested_update 

44from traitlets.utils.text import indent, wrap_paragraphs 

45 

46from ..utils import cast_unicode 

47from ..utils.importstring import import_item 

48 

49# ----------------------------------------------------------------------------- 

50# Descriptions for the various sections 

51# ----------------------------------------------------------------------------- 

52# merge flags&aliases into options 

53option_description = """ 

54The options below are convenience aliases to configurable class-options, 

55as listed in the "Equivalent to" description-line of the aliases. 

56To see all configurable class-options for some <cmd>, use: 

57 <cmd> --help-all 

58""".strip() # trim newlines of front and back 

59 

60keyvalue_description = """ 

61The command-line option below sets the respective configurable class-parameter: 

62 --Class.parameter=value 

63This line is evaluated in Python, so simple expressions are allowed. 

64For instance, to set `C.a=[0,1,2]`, you may type this: 

65 --C.a='range(3)' 

66""".strip() # trim newlines of front and back 

67 

68# sys.argv can be missing, for example when python is embedded. See the docs 

69# for details: http://docs.python.org/2/c-api/intro.html#embedding-python 

70if not hasattr(sys, "argv"): 

71 sys.argv = [""] 

72 

73subcommand_description = """ 

74Subcommands are launched as `{app} cmd [args]`. For information on using 

75subcommand 'cmd', do: `{app} cmd -h`. 

76""" 

77# get running program name 

78 

79# ----------------------------------------------------------------------------- 

80# Application class 

81# ----------------------------------------------------------------------------- 

82 

83 

84_envvar = os.environ.get("TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR", "") 

85if _envvar.lower() in {"1", "true"}: 

86 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = True 

87elif _envvar.lower() in {"0", "false", ""}: 

88 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR = False 

89else: 

90 raise ValueError( 

91 "Unsupported value for environment variable: 'TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR' is set to '%s' which is none of {'0', '1', 'false', 'true', ''}." 

92 % _envvar 

93 ) 

94 

95 

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

97 

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

99 

100 

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

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

103 

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

105 message, and exit the app. 

106 

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

108 """ 

109 

110 @functools.wraps(method) 

111 def inner(app, *args, **kwargs): 

112 try: 

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

114 except (TraitError, ArgumentError) as e: 

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

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

117 app.exit(1) 

118 

119 return cast(T, inner) 

120 

121 

122class ApplicationError(Exception): 

123 pass 

124 

125 

126class LevelFormatter(logging.Formatter): 

127 """Formatter with additional `highlevel` record 

128 

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

130 otherwise it is formatted with self.highlevel_format. 

131 

132 Useful for adding 'WARNING' to warning messages, 

133 without adding 'INFO' to info, etc. 

134 """ 

135 

136 highlevel_limit = logging.WARN 

137 highlevel_format = " %(levelname)s |" 

138 

139 def format(self, record): 

140 if record.levelno >= self.highlevel_limit: 

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

142 else: 

143 record.highlevel = "" 

144 return super().format(record) 

145 

146 

147class Application(SingletonConfigurable): 

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

149 

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

151 # line application 

152 name: t.Union[str, Unicode] = Unicode("application") 

153 

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

155 # of the help. 

156 description: t.Union[str, Unicode] = Unicode("This is an application.") 

157 # default section descriptions 

158 option_description: t.Union[str, Unicode] = Unicode(option_description) 

159 keyvalue_description: t.Union[str, Unicode] = Unicode(keyvalue_description) 

160 subcommand_description: t.Union[str, Unicode] = Unicode(subcommand_description) 

161 

162 python_config_loader_class = PyFileConfigLoader 

163 json_config_loader_class = JSONFileConfigLoader 

164 

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

166 examples: t.Union[str, Unicode] = Unicode() 

167 

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

169 # be exposed at the command line. 

170 classes: t.List[t.Type[t.Any]] = [] 

171 

172 def _classes_inc_parents(self, classes=None): 

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

174 

175 :param classes: 

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

177 

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

179 yielded once. 

180 """ 

181 if classes is None: 

182 classes = self.classes 

183 

184 seen = set() 

185 for c in classes: 

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

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

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

189 seen.add(parent) 

190 yield parent 

191 

192 # The version string of this application. 

193 version: t.Union[str, Unicode] = Unicode("0.0") 

194 

195 # the argv used to initialize the application 

196 argv: t.Union[t.List[str], List] = List() 

197 

198 # Whether failing to load config files should prevent startup 

199 raise_config_file_errors: t.Union[bool, Bool] = Bool( 

200 TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR 

201 ) 

202 

203 # The log level for the application 

204 log_level: t.Union[str, int, Enum] = Enum( 

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

206 default_value=logging.WARN, 

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

208 ).tag(config=True) 

209 

210 _log_formatter_cls = LevelFormatter 

211 

212 log_datefmt: t.Union[str, Unicode] = Unicode( 

213 "%Y-%m-%d %H:%M:%S", help="The date format used by logging formatters for %(asctime)s" 

214 ).tag(config=True) 

215 

216 log_format: t.Union[str, Unicode] = Unicode( 

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

218 help="The Logging format template", 

219 ).tag(config=True) 

220 

221 def get_default_logging_config(self): 

222 """Return the base logging configuration. 

223 

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

225 handler already exists. 

226 

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

228 by setting the ``log_level`` attribute. 

229 

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

231 control of logging. 

232 

233 """ 

234 config: t.Dict[str, t.Any] = { 

235 "version": 1, 

236 "handlers": { 

237 "console": { 

238 "class": "logging.StreamHandler", 

239 "formatter": "console", 

240 "level": logging.getLevelName(self.log_level), 

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

242 }, 

243 }, 

244 "formatters": { 

245 "console": { 

246 "class": ( 

247 f"{self._log_formatter_cls.__module__}" 

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

249 ), 

250 "format": self.log_format, 

251 "datefmt": self.log_datefmt, 

252 }, 

253 }, 

254 "loggers": { 

255 self.__class__.__name__: { 

256 "level": "DEBUG", 

257 "handlers": ["console"], 

258 } 

259 }, 

260 "disable_existing_loggers": False, 

261 } 

262 

263 if IS_PYTHONW: 

264 # disable logging 

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

266 # hooked up in parallel applications) 

267 del config["handlers"] 

268 del config["loggers"] 

269 

270 return config 

271 

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

273 def _observe_logging_change(self, change): 

274 # convert log level strings to ints 

275 log_level = self.log_level 

276 if isinstance(log_level, str): 

277 self.log_level = getattr(logging, log_level) 

278 self._configure_logging() 

279 

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

281 def _observe_logging_default(self, change): 

282 self._configure_logging() 

283 

284 def _configure_logging(self): 

285 config = self.get_default_logging_config() 

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

287 dictConfig(config) 

288 # make a note that we have configured logging 

289 self._logging_configured = True 

290 

291 @default("log") 

292 def _log_default(self): 

293 """Start logging for this application.""" 

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

295 log.propagate = False 

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

297 while _log: 

298 if _log.handlers: 

299 return log 

300 if not _log.propagate: 

301 break 

302 else: 

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

304 return log 

305 

306 logging_config = Dict( 

307 help=""" 

308 Configure additional log handlers. 

309 

310 The default stderr logs handler is configured by the 

311 log_level, log_datefmt and log_format settings. 

312 

313 This configuration can be used to configure additional handlers 

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

315 default handlers. 

316 

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

318 more information see: 

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

320 

321 This dictionary is merged with the base logging configuration which 

322 defines the following: 

323 

324 * A logging formatter intended for interactive use called 

325 ``console``. 

326 * A logging handler that writes to stderr called 

327 ``console`` which uses the formatter ``console``. 

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

329 level. 

330 

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

332 

333 .. code-block:: python 

334 

335 c.Application.logging_config = { 

336 'handlers': { 

337 'file': { 

338 'class': 'logging.FileHandler', 

339 'level': 'DEBUG', 

340 'filename': '<path/to/file>', 

341 } 

342 }, 

343 'loggers': { 

344 '<application-name>': { 

345 'level': 'DEBUG', 

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

347 # handler here then it will be disabled 

348 'handlers': ['console', 'file'], 

349 }, 

350 } 

351 } 

352 

353 """, 

354 ).tag(config=True) 

355 

356 #: the alias map for configurables 

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

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

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

360 # corresponding trait 

361 aliases: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Union[str, t.Tuple[str, str]]] = { 

362 "log-level": "Application.log_level" 

363 } 

364 

365 # flags for loading Configurables or store_const style flags 

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

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

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

369 flags: t.Dict[ 

370 t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict[str, t.Any], Config], str] 

371 ] = { 

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: t.Union[t.Dict[str, t.Tuple[t.Any, str]], Dict] = 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: t.Union[t.List[str], List] = 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() 

422 

423 show_config: t.Union[bool, Bool] = Bool( 

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

425 ).tag(config=True) 

426 

427 show_config_json: t.Union[bool, Bool] = 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): 

433 self.show_config = change.new 

434 

435 @observe("show_config") 

436 def _show_config_changed(self, change): 

437 if change.new: 

438 self._save_start = self.start 

439 self.start = self.start_show_config # type:ignore[assignment] 

440 

441 def __init__(self, **kwargs): 

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

456 super()._config_changed(change) 

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

458 

459 @catch_config_error 

460 def initialize(self, argv=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): 

468 """Start the app mainloop. 

469 

470 Override in subclasses. 

471 """ 

472 if self.subapp is not None: 

473 return self.subapp.start() 

474 

475 def start_show_config(self): 

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: t.Dict[str, t.Any] = dict(indent=4, compact=True) 

503 

504 for traitname in sorted(class_config): 

505 value = class_config[traitname] 

506 print( 

507 " .{} = {}".format( 

508 traitname, 

509 pprint.pformat(value, **pformat_kwargs), 

510 ) 

511 ) 

512 

513 def print_alias_help(self): 

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

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

516 

517 def emit_alias_help(self): 

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

519 if not self.aliases: 

520 return 

521 

522 classdict = {} 

523 for cls in self.classes: 

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

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

526 classdict[c.__name__] = c 

527 

528 fhelp: t.Optional[str] 

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

530 try: 

531 if isinstance(longname, tuple): 

532 longname, fhelp = longname 

533 else: 

534 fhelp = None 

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

536 longname = classname + "." + traitname 

537 cls = classdict[classname] 

538 

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

540 fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines() 

541 

542 if not isinstance(alias, tuple): 

543 alias = (alias,) 

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

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

546 

547 # reformat first line 

548 assert fhelp is not None 

549 fhelp[0] = fhelp[0].replace("--" + longname, alias) # type:ignore 

550 yield from fhelp 

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

552 except Exception as ex: 

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

554 raise 

555 

556 def print_flag_help(self): 

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

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

559 

560 def emit_flag_help(self): 

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

562 if not self.flags: 

563 return 

564 

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

566 try: 

567 if not isinstance(flags, tuple): 

568 flags = (flags,) 

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

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

571 yield flags 

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

573 cfg_list = " ".join( 

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

575 for clname, props_dict in cfg.items() 

576 for prop, val in props_dict.items() 

577 ) 

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

579 yield indent(dedent(cfg_txt)) 

580 except Exception as ex: 

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

582 raise 

583 

584 def print_options(self): 

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

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

587 

588 def emit_options_help(self): 

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

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

591 return 

592 header = "Options" 

593 yield header 

594 yield "=" * len(header) 

595 for p in wrap_paragraphs(self.option_description): 

596 yield p 

597 yield "" 

598 

599 yield from self.emit_flag_help() 

600 yield from self.emit_alias_help() 

601 yield "" 

602 

603 def print_subcommands(self): 

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

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

606 

607 def emit_subcommands_help(self): 

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

609 if not self.subcommands: 

610 return 

611 

612 header = "Subcommands" 

613 yield header 

614 yield "=" * len(header) 

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

616 yield p 

617 yield "" 

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

619 yield subc 

620 if help: 

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

622 yield "" 

623 

624 def emit_help_epilogue(self, classes): 

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

626 

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

628 """ 

629 if not classes: 

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

631 yield "" 

632 

633 def print_help(self, classes=False): 

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

635 

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

637 """ 

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

639 

640 def emit_help(self, classes=False): 

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

642 

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

644 """ 

645 yield from self.emit_description() 

646 yield from self.emit_subcommands_help() 

647 yield from self.emit_options_help() 

648 

649 if classes: 

650 help_classes = self._classes_with_config_traits() 

651 if help_classes: 

652 yield "Class options" 

653 yield "=============" 

654 for p in wrap_paragraphs(self.keyvalue_description): 

655 yield p 

656 yield "" 

657 

658 for cls in help_classes: 

659 yield cls.class_get_help() 

660 yield "" 

661 yield from self.emit_examples() 

662 

663 yield from self.emit_help_epilogue(classes) 

664 

665 def document_config_options(self): 

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

667 

668 Returns a multiline string. 

669 """ 

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

671 

672 def print_description(self): 

673 """Print the application description.""" 

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

675 

676 def emit_description(self): 

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

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

679 yield p 

680 yield "" 

681 

682 def print_examples(self): 

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

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

685 

686 def emit_examples(self): 

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

688 

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

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

691 """ 

692 if self.examples: 

693 yield "Examples" 

694 yield "--------" 

695 yield "" 

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

697 yield "" 

698 

699 def print_version(self): 

700 """Print the version string.""" 

701 print(self.version) 

702 

703 @catch_config_error 

704 def initialize_subcommand(self, subc, argv=None): 

705 """Initialize a subcommand with argv.""" 

706 val = self.subcommands.get(subc) 

707 assert val is not None 

708 subapp, _ = val 

709 

710 if isinstance(subapp, str): 

711 subapp = import_item(subapp) 

712 

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

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

715 # Clear existing instances before... 

716 self.__class__.clear_instance() 

717 # instantiating subapp... 

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

719 elif callable(subapp): 

720 # or ask factory to create it... 

721 self.subapp = subapp(self) # type:ignore[call-arg] 

722 else: 

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

724 

725 # ... and finally initialize subapp. 

726 self.subapp.initialize(argv) 

727 

728 def flatten_flags(self): 

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

730 

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

732 but a config file setting the same trait in TerminalInteraciveShell 

733 getting inappropriate priority over the command-line arg. 

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

735 

736 Only aliases with exactly one descendent in the class list 

737 will be promoted. 

738 

739 """ 

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

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

742 # that are descendents 

743 mro_tree = defaultdict(list) 

744 for cls in self.classes: 

745 clsname = cls.__name__ 

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

747 # exclude cls itself and Configurable,HasTraits,object 

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

749 # flatten aliases, which have the form: 

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

751 aliases: t.Dict[str, str] = {} 

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

753 if isinstance(longname, tuple): 

754 longname, _ = longname 

755 cls, trait = longname.split(".", 1) # type:ignore 

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

757 if len(children) == 1: 

758 # exactly one descendent, promote alias 

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

760 if not isinstance(aliases, tuple): 

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

762 for al in alias: 

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

764 

765 # flatten flags, which are of the form: 

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

767 flags = {} 

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

769 newflag: t.Dict[t.Any, t.Any] = {} 

770 for cls, subdict in flagdict.items(): # type:ignore 

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

772 # exactly one descendent, promote flag section 

773 if len(children) == 1: 

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

775 

776 if cls in newflag: 

777 newflag[cls].update(subdict) 

778 else: 

779 newflag[cls] = subdict 

780 

781 if not isinstance(key, tuple): 

782 key = (key,) 

783 for k in key: 

784 flags[k] = (newflag, help) 

785 return flags, aliases 

786 

787 def _create_loader(self, argv, aliases, flags, classes): 

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) -> t.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): 

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=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 = tuple(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(cls, basefilename, path=None, log=None, raise_config_file_errors=False): 

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

892 

893 yield each config object in turn. 

894 """ 

895 

896 if not isinstance(path, list): 

897 path = [path] 

898 for current in reversed(path): 

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

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

901 if log: 

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

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

904 loaded: t.List[t.Any] = [] 

905 filenames: t.List[str] = [] 

906 for loader in [pyloader, jsonloader]: 

907 config = None 

908 try: 

909 config = loader.load_config() 

910 except ConfigFileNotFound: 

911 pass 

912 except Exception: 

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

914 # unlikely event that the error raised before filefind finished 

915 filename = loader.full_filename or basefilename 

916 # problem while running the file 

917 if raise_config_file_errors: 

918 raise 

919 if log: 

920 log.error("Exception while loading config file %s", filename, exc_info=True) 

921 else: 

922 if log: 

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

924 if config: 

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

926 collisions = earlier_config.collisions(config) 

927 if collisions and log: 

928 log.warning( 

929 "Collisions detected in {0} and {1} config files." 

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

931 filename, 

932 loader.full_filename, 

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

934 ) 

935 ) 

936 yield (config, loader.full_filename) 

937 loaded.append(config) 

938 filenames.append(loader.full_filename) 

939 

940 @property 

941 def loaded_config_files(self): 

942 """Currently loaded configuration files""" 

943 return self._loaded_config_files[:] 

944 

945 @catch_config_error 

946 def load_config_file(self, filename, path=None): 

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

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

949 new_config = Config() 

950 for (config, fname) in self._load_config_files( 

951 filename, 

952 path=path, 

953 log=self.log, 

954 raise_config_file_errors=self.raise_config_file_errors, 

955 ): 

956 new_config.merge(config) 

957 if ( 

958 fname not in self._loaded_config_files 

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

960 self._loaded_config_files.append(fname) 

961 # add self.cli_config to preserve CLI config priority 

962 new_config.merge(self.cli_config) 

963 self.update_config(new_config) 

964 

965 def _classes_with_config_traits(self, classes=None): 

966 """ 

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

968 

969 :param classes: 

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

971 

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

973 on which a trait-value may be overridden: 

974 

975 - either on the class owning the trait, 

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

977 any traits themselves. 

978 """ 

979 if classes is None: 

980 classes = self.classes 

981 

982 cls_to_config = OrderedDict( 

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

984 for cls in self._classes_inc_parents(classes) 

985 ) 

986 

987 def is_any_parent_included(cls): 

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

989 

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

991 # and loop until no more classes gets marked. 

992 # 

993 while True: 

994 to_incl_orig = cls_to_config.copy() 

995 cls_to_config = OrderedDict( 

996 (cls, inc_yes or is_any_parent_included(cls)) 

997 for cls, inc_yes in cls_to_config.items() 

998 ) 

999 if cls_to_config == to_incl_orig: 

1000 break 

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

1002 if inc_yes: 

1003 yield cl 

1004 

1005 def generate_config_file(self, classes=None): 

1006 """generate default config file from Configurables""" 

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

1008 lines.append("") 

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

1010 lines.append("") 

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

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

1013 for cls in config_classes: 

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

1015 return "\n".join(lines) 

1016 

1017 def close_handlers(self): 

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

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

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

1021 # have not yet been initialised) 

1022 for handler in self.log.handlers: 

1023 with suppress(Exception): 

1024 handler.close() 

1025 self._logging_configured = False 

1026 

1027 def exit(self, exit_status=0): 

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

1029 self.close_handlers() 

1030 sys.exit(exit_status) 

1031 

1032 def __del__(self): 

1033 self.close_handlers() 

1034 

1035 @classmethod 

1036 def launch_instance(cls, argv=None, **kwargs): 

1037 """Launch a global instance of this Application 

1038 

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

1040 """ 

1041 app = cls.instance(**kwargs) 

1042 app.initialize(argv) 

1043 app.start() 

1044 

1045 

1046# ----------------------------------------------------------------------------- 

1047# utility functions, for convenience 

1048# ----------------------------------------------------------------------------- 

1049 

1050default_aliases = Application.aliases 

1051default_flags = Application.flags 

1052 

1053 

1054def boolean_flag(name, configurable, set_help="", unset_help=""): 

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

1056 

1057 Parameters 

1058 ---------- 

1059 name : str 

1060 The name of the flag. 

1061 configurable : str 

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

1063 set_help : unicode 

1064 help string for --name flag 

1065 unset_help : unicode 

1066 help string for --no-name flag 

1067 

1068 Returns 

1069 ------- 

1070 cfg : dict 

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

1072 the trait, respectively. 

1073 """ 

1074 # default helpstrings 

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

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

1077 

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

1079 

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

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

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

1083 

1084 

1085def get_config(): 

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

1087 

1088 otherwise return an empty config object 

1089 """ 

1090 if Application.initialized(): 

1091 return Application.instance().config 

1092 else: 

1093 return Config() 

1094 

1095 

1096if __name__ == "__main__": 

1097 Application.launch_instance()