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