fixed subscription table
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
from __future__ import annotations
|
||||
|
||||
from .application import *
|
||||
from .configurable import *
|
||||
from .loader import Config
|
||||
|
||||
__all__ = [ # noqa: F405
|
||||
"Config",
|
||||
"Application",
|
||||
"ApplicationError",
|
||||
"LevelFormatter",
|
||||
"configurable",
|
||||
"Configurable",
|
||||
"ConfigurableError",
|
||||
"MultipleInstanceError",
|
||||
"LoggingConfigurable",
|
||||
"SingletonConfigurable",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1129
.venv/lib/python3.12/site-packages/traitlets/config/application.py
Normal file
1129
.venv/lib/python3.12/site-packages/traitlets/config/application.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
"""Helper utilities for integrating argcomplete with traitlets"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import typing as t
|
||||
|
||||
try:
|
||||
import argcomplete
|
||||
from argcomplete import CompletionFinder # type:ignore[attr-defined]
|
||||
except ImportError:
|
||||
# This module and its utility methods are written to not crash even
|
||||
# if argcomplete is not installed.
|
||||
class StubModule:
|
||||
def __getattr__(self, attr: str) -> t.Any:
|
||||
if not attr.startswith("__"):
|
||||
raise ModuleNotFoundError("No module named 'argcomplete'")
|
||||
raise AttributeError(f"argcomplete stub module has no attribute '{attr}'")
|
||||
|
||||
argcomplete = StubModule() # type:ignore[assignment]
|
||||
CompletionFinder = object # type:ignore[assignment, misc]
|
||||
|
||||
|
||||
def get_argcomplete_cwords() -> t.Optional[t.List[str]]:
|
||||
"""Get current words prior to completion point
|
||||
|
||||
This is normally done in the `argcomplete.CompletionFinder` constructor,
|
||||
but is exposed here to allow `traitlets` to follow dynamic code-paths such
|
||||
as determining whether to evaluate a subcommand.
|
||||
"""
|
||||
if "_ARGCOMPLETE" not in os.environ:
|
||||
return None
|
||||
|
||||
comp_line = os.environ["COMP_LINE"]
|
||||
comp_point = int(os.environ["COMP_POINT"])
|
||||
# argcomplete.debug("splitting COMP_LINE for:", comp_line, comp_point)
|
||||
comp_words: t.List[str]
|
||||
try:
|
||||
(
|
||||
cword_prequote,
|
||||
cword_prefix,
|
||||
cword_suffix,
|
||||
comp_words,
|
||||
last_wordbreak_pos,
|
||||
) = argcomplete.split_line(comp_line, comp_point) # type:ignore[attr-defined,no-untyped-call]
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
|
||||
# _ARGCOMPLETE is set by the shell script to tell us where comp_words
|
||||
# should start, based on what we're completing.
|
||||
# 1: <script> [args]
|
||||
# 2: python <script> [args]
|
||||
# 3: python -m <module> [args]
|
||||
start = int(os.environ["_ARGCOMPLETE"]) - 1
|
||||
comp_words = comp_words[start:]
|
||||
|
||||
# argcomplete.debug("prequote=", cword_prequote, "prefix=", cword_prefix, "suffix=", cword_suffix, "words=", comp_words, "last=", last_wordbreak_pos)
|
||||
return comp_words # noqa: RET504
|
||||
|
||||
|
||||
def increment_argcomplete_index() -> None:
|
||||
"""Assumes ``$_ARGCOMPLETE`` is set and `argcomplete` is importable
|
||||
|
||||
Increment the index pointed to by ``$_ARGCOMPLETE``, which is used to
|
||||
determine which word `argcomplete` should start evaluating the command-line.
|
||||
This may be useful to "inform" `argcomplete` that we have already evaluated
|
||||
the first word as a subcommand.
|
||||
"""
|
||||
try:
|
||||
os.environ["_ARGCOMPLETE"] = str(int(os.environ["_ARGCOMPLETE"]) + 1)
|
||||
except Exception:
|
||||
try:
|
||||
argcomplete.debug("Unable to increment $_ARGCOMPLETE", os.environ["_ARGCOMPLETE"]) # type:ignore[attr-defined,no-untyped-call]
|
||||
except (KeyError, ModuleNotFoundError):
|
||||
pass
|
||||
|
||||
|
||||
class ExtendedCompletionFinder(CompletionFinder):
|
||||
"""An extension of CompletionFinder which dynamically completes class-trait based options
|
||||
|
||||
This finder adds a few functionalities:
|
||||
|
||||
1. When completing options, it will add ``--Class.`` to the list of completions, for each
|
||||
class in `Application.classes` that could complete the current option.
|
||||
2. If it detects that we are currently trying to complete an option related to ``--Class.``,
|
||||
it will add the corresponding config traits of Class to the `ArgumentParser` instance,
|
||||
so that the traits' completers can be used.
|
||||
3. If there are any subcommands, they are added as completions for the first word
|
||||
|
||||
Note that we are avoiding adding all config traits of all classes to the `ArgumentParser`,
|
||||
which would be easier but would add more runtime overhead and would also make completions
|
||||
appear more spammy.
|
||||
|
||||
These changes do require using the internals of `argcomplete.CompletionFinder`.
|
||||
"""
|
||||
|
||||
_parser: argparse.ArgumentParser
|
||||
config_classes: t.List[t.Any] = [] # Configurables
|
||||
subcommands: t.List[str] = []
|
||||
|
||||
def match_class_completions(self, cword_prefix: str) -> t.List[t.Tuple[t.Any, str]]:
|
||||
"""Match the word to be completed against our Configurable classes
|
||||
|
||||
Check if cword_prefix could potentially match against --{class}. for any class
|
||||
in Application.classes.
|
||||
"""
|
||||
class_completions = [(cls, f"--{cls.__name__}.") for cls in self.config_classes]
|
||||
matched_completions = class_completions
|
||||
if "." in cword_prefix:
|
||||
cword_prefix = cword_prefix[: cword_prefix.index(".") + 1]
|
||||
matched_completions = [(cls, c) for (cls, c) in class_completions if c == cword_prefix]
|
||||
elif len(cword_prefix) > 0:
|
||||
matched_completions = [
|
||||
(cls, c) for (cls, c) in class_completions if c.startswith(cword_prefix)
|
||||
]
|
||||
return matched_completions
|
||||
|
||||
def inject_class_to_parser(self, cls: t.Any) -> None:
|
||||
"""Add dummy arguments to our ArgumentParser for the traits of this class
|
||||
|
||||
The argparse-based loader currently does not actually add any class traits to
|
||||
the constructed ArgumentParser, only the flags & aliaes. In order to work nicely
|
||||
with argcomplete's completers functionality, this method adds dummy arguments
|
||||
of the form --Class.trait to the ArgumentParser instance.
|
||||
|
||||
This method should be called selectively to reduce runtime overhead and to avoid
|
||||
spamming options across all of Application.classes.
|
||||
"""
|
||||
try:
|
||||
for traitname, trait in cls.class_traits(config=True).items():
|
||||
completer = trait.metadata.get("argcompleter") or getattr(
|
||||
trait, "argcompleter", None
|
||||
)
|
||||
multiplicity = trait.metadata.get("multiplicity")
|
||||
self._parser.add_argument( # type: ignore[attr-defined]
|
||||
f"--{cls.__name__}.{traitname}",
|
||||
type=str,
|
||||
help=trait.help,
|
||||
nargs=multiplicity,
|
||||
# metavar=traitname,
|
||||
).completer = completer
|
||||
# argcomplete.debug(f"added --{cls.__name__}.{traitname}")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def _get_completions(
|
||||
self, comp_words: t.List[str], cword_prefix: str, *args: t.Any
|
||||
) -> t.List[str]:
|
||||
"""Overridden to dynamically append --Class.trait arguments if appropriate
|
||||
|
||||
Warning:
|
||||
This does not (currently) support completions of the form
|
||||
--Class1.Class2.<...>.trait, although this is valid for traitlets.
|
||||
Part of the reason is that we don't currently have a way to identify
|
||||
which classes may be used with Class1 as a parent.
|
||||
|
||||
Warning:
|
||||
This is an internal method in CompletionFinder and so the API might
|
||||
be subject to drift.
|
||||
"""
|
||||
# Try to identify if we are completing something related to --Class. for
|
||||
# a known Class, if we are then add the Class config traits to our ArgumentParser.
|
||||
prefix_chars = self._parser.prefix_chars
|
||||
is_option = len(cword_prefix) > 0 and cword_prefix[0] in prefix_chars
|
||||
if is_option:
|
||||
# If we are currently completing an option, check if it could
|
||||
# match with any of the --Class. completions. If there's exactly
|
||||
# one matched class, then expand out the --Class.trait options.
|
||||
matched_completions = self.match_class_completions(cword_prefix)
|
||||
if len(matched_completions) == 1:
|
||||
matched_cls = matched_completions[0][0]
|
||||
self.inject_class_to_parser(matched_cls)
|
||||
elif len(comp_words) > 0 and "." in comp_words[-1] and not is_option:
|
||||
# If not an option, perform a hacky check to see if we are completing
|
||||
# an argument for an already present --Class.trait option. Search backwards
|
||||
# for last option (based on last word starting with prefix_chars), and see
|
||||
# if it is of the form --Class.trait. Note that if multiplicity="+", these
|
||||
# arguments might conflict with positional arguments.
|
||||
for prev_word in comp_words[::-1]:
|
||||
if len(prev_word) > 0 and prev_word[0] in prefix_chars:
|
||||
matched_completions = self.match_class_completions(prev_word)
|
||||
if matched_completions:
|
||||
matched_cls = matched_completions[0][0]
|
||||
self.inject_class_to_parser(matched_cls)
|
||||
break
|
||||
|
||||
completions: t.List[str]
|
||||
completions = super()._get_completions(comp_words, cword_prefix, *args) # type:ignore[no-untyped-call]
|
||||
|
||||
# For subcommand-handling: it is difficult to get this to work
|
||||
# using argparse subparsers, because the ArgumentParser accepts
|
||||
# arbitrary extra_args, which ends up masking subparsers.
|
||||
# Instead, check if comp_words only consists of the script,
|
||||
# if so check if any subcommands start with cword_prefix.
|
||||
if self.subcommands and len(comp_words) == 1:
|
||||
argcomplete.debug("Adding subcommands for", cword_prefix) # type:ignore[attr-defined,no-untyped-call]
|
||||
completions.extend(subc for subc in self.subcommands if subc.startswith(cword_prefix))
|
||||
|
||||
return completions
|
||||
|
||||
def _get_option_completions(
|
||||
self, parser: argparse.ArgumentParser, cword_prefix: str
|
||||
) -> t.List[str]:
|
||||
"""Overridden to add --Class. completions when appropriate"""
|
||||
completions: t.List[str]
|
||||
completions = super()._get_option_completions(parser, cword_prefix) # type:ignore[no-untyped-call]
|
||||
if cword_prefix.endswith("."):
|
||||
return completions
|
||||
|
||||
matched_completions = self.match_class_completions(cword_prefix)
|
||||
if len(matched_completions) > 1:
|
||||
completions.extend(opt for cls, opt in matched_completions)
|
||||
# If there is exactly one match, we would expect it to have already
|
||||
# been handled by the options dynamically added in _get_completions().
|
||||
# However, maybe there's an edge cases missed here, for example if the
|
||||
# matched class has no configurable traits.
|
||||
return completions
|
@@ -0,0 +1,600 @@
|
||||
"""A base class for objects that are configurable."""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import typing as t
|
||||
from copy import deepcopy
|
||||
from textwrap import dedent
|
||||
|
||||
from traitlets.traitlets import (
|
||||
Any,
|
||||
Container,
|
||||
Dict,
|
||||
HasTraits,
|
||||
Instance,
|
||||
TraitType,
|
||||
default,
|
||||
observe,
|
||||
observe_compat,
|
||||
validate,
|
||||
)
|
||||
from traitlets.utils import warnings
|
||||
from traitlets.utils.bunch import Bunch
|
||||
from traitlets.utils.text import indent, wrap_paragraphs
|
||||
|
||||
from .loader import Config, DeferredConfig, LazyConfigValue, _is_section_key
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper classes for Configurables
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
LoggerType = t.Union[logging.Logger, logging.LoggerAdapter[t.Any]]
|
||||
else:
|
||||
LoggerType = t.Any
|
||||
|
||||
|
||||
class ConfigurableError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MultipleInstanceError(ConfigurableError):
|
||||
pass
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Configurable implementation
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Configurable(HasTraits):
|
||||
config = Instance(Config, (), {})
|
||||
parent = Instance("traitlets.config.configurable.Configurable", allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs: t.Any) -> None:
|
||||
"""Create a configurable given a config config.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
config : Config
|
||||
If this is empty, default values are used. If config is a
|
||||
:class:`Config` instance, it will be used to configure the
|
||||
instance.
|
||||
parent : Configurable instance, optional
|
||||
The parent Configurable instance of this object.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Subclasses of Configurable must call the :meth:`__init__` method of
|
||||
:class:`Configurable` *before* doing anything else and using
|
||||
:func:`super`::
|
||||
|
||||
class MyConfigurable(Configurable):
|
||||
def __init__(self, config=None):
|
||||
super(MyConfigurable, self).__init__(config=config)
|
||||
# Then any other code you need to finish initialization.
|
||||
|
||||
This ensures that instances will be configured properly.
|
||||
"""
|
||||
parent = kwargs.pop("parent", None)
|
||||
if parent is not None:
|
||||
# config is implied from parent
|
||||
if kwargs.get("config", None) is None:
|
||||
kwargs["config"] = parent.config
|
||||
self.parent = parent
|
||||
|
||||
config = kwargs.pop("config", None)
|
||||
|
||||
# load kwarg traits, other than config
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# record traits set by config
|
||||
config_override_names = set()
|
||||
|
||||
def notice_config_override(change: Bunch) -> None:
|
||||
"""Record traits set by both config and kwargs.
|
||||
|
||||
They will need to be overridden again after loading config.
|
||||
"""
|
||||
if change.name in kwargs:
|
||||
config_override_names.add(change.name)
|
||||
|
||||
self.observe(notice_config_override)
|
||||
|
||||
# load config
|
||||
if config is not None:
|
||||
# We used to deepcopy, but for now we are trying to just save
|
||||
# by reference. This *could* have side effects as all components
|
||||
# will share config. In fact, I did find such a side effect in
|
||||
# _config_changed below. If a config attribute value was a mutable type
|
||||
# all instances of a component were getting the same copy, effectively
|
||||
# making that a class attribute.
|
||||
# self.config = deepcopy(config)
|
||||
self.config = config
|
||||
else:
|
||||
# allow _config_default to return something
|
||||
self._load_config(self.config)
|
||||
self.unobserve(notice_config_override)
|
||||
|
||||
for name in config_override_names:
|
||||
setattr(self, name, kwargs[name])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Static trait notifications
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def section_names(cls) -> list[str]:
|
||||
"""return section names as a list"""
|
||||
return [
|
||||
c.__name__
|
||||
for c in reversed(cls.__mro__)
|
||||
if issubclass(c, Configurable) and issubclass(cls, c)
|
||||
]
|
||||
|
||||
def _find_my_config(self, cfg: Config) -> t.Any:
|
||||
"""extract my config from a global Config object
|
||||
|
||||
will construct a Config object of only the config values that apply to me
|
||||
based on my mro(), as well as those of my parent(s) if they exist.
|
||||
|
||||
If I am Bar and my parent is Foo, and their parent is Tim,
|
||||
this will return merge following config sections, in this order::
|
||||
|
||||
[Bar, Foo.Bar, Tim.Foo.Bar]
|
||||
|
||||
With the last item being the highest priority.
|
||||
"""
|
||||
cfgs = [cfg]
|
||||
if self.parent:
|
||||
cfgs.append(self.parent._find_my_config(cfg))
|
||||
my_config = Config()
|
||||
for c in cfgs:
|
||||
for sname in self.section_names():
|
||||
# Don't do a blind getattr as that would cause the config to
|
||||
# dynamically create the section with name Class.__name__.
|
||||
if c._has_section(sname):
|
||||
my_config.merge(c[sname])
|
||||
return my_config
|
||||
|
||||
def _load_config(
|
||||
self,
|
||||
cfg: Config,
|
||||
section_names: list[str] | None = None,
|
||||
traits: dict[str, TraitType[t.Any, t.Any]] | None = None,
|
||||
) -> None:
|
||||
"""load traits from a Config object"""
|
||||
|
||||
if traits is None:
|
||||
traits = self.traits(config=True)
|
||||
if section_names is None:
|
||||
section_names = self.section_names()
|
||||
|
||||
my_config = self._find_my_config(cfg)
|
||||
|
||||
# hold trait notifications until after all config has been loaded
|
||||
with self.hold_trait_notifications():
|
||||
for name, config_value in my_config.items():
|
||||
if name in traits:
|
||||
if isinstance(config_value, LazyConfigValue):
|
||||
# ConfigValue is a wrapper for using append / update on containers
|
||||
# without having to copy the initial value
|
||||
initial = getattr(self, name)
|
||||
config_value = config_value.get_value(initial)
|
||||
elif isinstance(config_value, DeferredConfig):
|
||||
# DeferredConfig tends to come from CLI/environment variables
|
||||
config_value = config_value.get_value(traits[name])
|
||||
# We have to do a deepcopy here if we don't deepcopy the entire
|
||||
# config object. If we don't, a mutable config_value will be
|
||||
# shared by all instances, effectively making it a class attribute.
|
||||
setattr(self, name, deepcopy(config_value))
|
||||
elif not _is_section_key(name) and not isinstance(config_value, Config):
|
||||
from difflib import get_close_matches
|
||||
|
||||
if isinstance(self, LoggingConfigurable):
|
||||
assert self.log is not None
|
||||
warn = self.log.warning
|
||||
else:
|
||||
|
||||
def warn(msg: t.Any) -> None:
|
||||
return warnings.warn(msg, UserWarning, stacklevel=9)
|
||||
|
||||
matches = get_close_matches(name, traits)
|
||||
msg = f"Config option `{name}` not recognized by `{self.__class__.__name__}`."
|
||||
|
||||
if len(matches) == 1:
|
||||
msg += f" Did you mean `{matches[0]}`?"
|
||||
elif len(matches) >= 1:
|
||||
msg += " Did you mean one of: `{matches}`?".format(
|
||||
matches=", ".join(sorted(matches))
|
||||
)
|
||||
warn(msg)
|
||||
|
||||
@observe("config")
|
||||
@observe_compat
|
||||
def _config_changed(self, change: Bunch) -> None:
|
||||
"""Update all the class traits having ``config=True`` in metadata.
|
||||
|
||||
For any class trait with a ``config`` metadata attribute that is
|
||||
``True``, we update the trait with the value of the corresponding
|
||||
config entry.
|
||||
"""
|
||||
# Get all traits with a config metadata entry that is True
|
||||
traits = self.traits(config=True)
|
||||
|
||||
# We auto-load config section for this class as well as any parent
|
||||
# classes that are Configurable subclasses. This starts with Configurable
|
||||
# and works down the mro loading the config for each section.
|
||||
section_names = self.section_names()
|
||||
self._load_config(change.new, traits=traits, section_names=section_names)
|
||||
|
||||
def update_config(self, config: Config) -> None:
|
||||
"""Update config and load the new values"""
|
||||
# traitlets prior to 4.2 created a copy of self.config in order to trigger change events.
|
||||
# Some projects (IPython < 5) relied upon one side effect of this,
|
||||
# that self.config prior to update_config was not modified in-place.
|
||||
# For backward-compatibility, we must ensure that self.config
|
||||
# is a new object and not modified in-place,
|
||||
# but config consumers should not rely on this behavior.
|
||||
self.config = deepcopy(self.config)
|
||||
# load config
|
||||
self._load_config(config)
|
||||
# merge it into self.config
|
||||
self.config.merge(config)
|
||||
# TODO: trigger change event if/when dict-update change events take place
|
||||
# DO NOT trigger full trait-change
|
||||
|
||||
@classmethod
|
||||
def class_get_help(cls, inst: HasTraits | None = None) -> str:
|
||||
"""Get the help string for this class in ReST format.
|
||||
|
||||
If `inst` is given, its current trait values will be used in place of
|
||||
class defaults.
|
||||
"""
|
||||
assert inst is None or isinstance(inst, cls)
|
||||
final_help = []
|
||||
base_classes = ", ".join(p.__name__ for p in cls.__bases__)
|
||||
final_help.append(f"{cls.__name__}({base_classes}) options")
|
||||
final_help.append(len(final_help[0]) * "-")
|
||||
for _, v in sorted(cls.class_traits(config=True).items()):
|
||||
help = cls.class_get_trait_help(v, inst)
|
||||
final_help.append(help)
|
||||
return "\n".join(final_help)
|
||||
|
||||
@classmethod
|
||||
def class_get_trait_help(
|
||||
cls,
|
||||
trait: TraitType[t.Any, t.Any],
|
||||
inst: HasTraits | None = None,
|
||||
helptext: str | None = None,
|
||||
) -> str:
|
||||
"""Get the helptext string for a single trait.
|
||||
|
||||
:param inst:
|
||||
If given, its current trait values will be used in place of
|
||||
the class default.
|
||||
:param helptext:
|
||||
If not given, uses the `help` attribute of the current trait.
|
||||
"""
|
||||
assert inst is None or isinstance(inst, cls)
|
||||
lines = []
|
||||
header = f"--{cls.__name__}.{trait.name}"
|
||||
if isinstance(trait, (Container, Dict)):
|
||||
multiplicity = trait.metadata.get("multiplicity", "append")
|
||||
if isinstance(trait, Dict):
|
||||
sample_value = "<key-1>=<value-1>"
|
||||
else:
|
||||
sample_value = "<%s-item-1>" % trait.__class__.__name__.lower()
|
||||
if multiplicity == "append":
|
||||
header = f"{header}={sample_value}..."
|
||||
else:
|
||||
header = f"{header} {sample_value}..."
|
||||
else:
|
||||
header = f"{header}=<{trait.__class__.__name__}>"
|
||||
# header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
|
||||
lines.append(header)
|
||||
|
||||
if helptext is None:
|
||||
helptext = trait.help
|
||||
if helptext != "":
|
||||
helptext = "\n".join(wrap_paragraphs(helptext, 76))
|
||||
lines.append(indent(helptext))
|
||||
|
||||
if "Enum" in trait.__class__.__name__:
|
||||
# include Enum choices
|
||||
lines.append(indent("Choices: %s" % trait.info()))
|
||||
|
||||
if inst is not None:
|
||||
lines.append(indent(f"Current: {getattr(inst, trait.name or '')!r}"))
|
||||
else:
|
||||
try:
|
||||
dvr = trait.default_value_repr()
|
||||
except Exception:
|
||||
dvr = None # ignore defaults we can't construct
|
||||
if dvr is not None:
|
||||
if len(dvr) > 64:
|
||||
dvr = dvr[:61] + "..."
|
||||
lines.append(indent("Default: %s" % dvr))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@classmethod
|
||||
def class_print_help(cls, inst: HasTraits | None = None) -> None:
|
||||
"""Get the help string for a single trait and print it."""
|
||||
print(cls.class_get_help(inst)) # noqa: T201
|
||||
|
||||
@classmethod
|
||||
def _defining_class(
|
||||
cls, trait: TraitType[t.Any, t.Any], classes: t.Sequence[type[HasTraits]]
|
||||
) -> type[Configurable]:
|
||||
"""Get the class that defines a trait
|
||||
|
||||
For reducing redundant help output in config files.
|
||||
Returns the current class if:
|
||||
- the trait is defined on this class, or
|
||||
- the class where it is defined would not be in the config file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trait : Trait
|
||||
The trait to look for
|
||||
classes : list
|
||||
The list of other classes to consider for redundancy.
|
||||
Will return `cls` even if it is not defined on `cls`
|
||||
if the defining class is not in `classes`.
|
||||
"""
|
||||
defining_cls = cls
|
||||
assert trait.name is not None
|
||||
for parent in cls.mro():
|
||||
if (
|
||||
issubclass(parent, Configurable)
|
||||
and parent in classes
|
||||
and parent.class_own_traits(config=True).get(trait.name, None) is trait
|
||||
):
|
||||
defining_cls = parent
|
||||
return defining_cls
|
||||
|
||||
@classmethod
|
||||
def class_config_section(cls, classes: t.Sequence[type[HasTraits]] | None = None) -> str:
|
||||
"""Get the config section for this class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
classes : list, optional
|
||||
The list of other classes in the config file.
|
||||
Used to reduce redundant information.
|
||||
"""
|
||||
|
||||
def c(s: str) -> str:
|
||||
"""return a commented, wrapped block."""
|
||||
s = "\n\n".join(wrap_paragraphs(s, 78))
|
||||
|
||||
return "## " + s.replace("\n", "\n# ")
|
||||
|
||||
# section header
|
||||
breaker = "#" + "-" * 78
|
||||
parent_classes = ", ".join(p.__name__ for p in cls.__bases__ if issubclass(p, Configurable))
|
||||
|
||||
s = f"# {cls.__name__}({parent_classes}) configuration"
|
||||
lines = [breaker, s, breaker]
|
||||
# get the description trait
|
||||
desc = cls.class_traits().get("description")
|
||||
if desc:
|
||||
desc = desc.default_value
|
||||
if not desc:
|
||||
# no description from trait, use __doc__
|
||||
desc = getattr(cls, "__doc__", "") # type:ignore[arg-type]
|
||||
if desc:
|
||||
lines.append(c(desc)) # type:ignore[arg-type]
|
||||
lines.append("")
|
||||
|
||||
for name, trait in sorted(cls.class_traits(config=True).items()):
|
||||
default_repr = trait.default_value_repr()
|
||||
|
||||
if classes:
|
||||
defining_class = cls._defining_class(trait, classes)
|
||||
else:
|
||||
defining_class = cls
|
||||
if defining_class is cls:
|
||||
# cls owns the trait, show full help
|
||||
if trait.help:
|
||||
lines.append(c(trait.help))
|
||||
if "Enum" in type(trait).__name__:
|
||||
# include Enum choices
|
||||
lines.append("# Choices: %s" % trait.info())
|
||||
lines.append("# Default: %s" % default_repr)
|
||||
else:
|
||||
# Trait appears multiple times and isn't defined here.
|
||||
# Truncate help to first line + "See also Original.trait"
|
||||
if trait.help:
|
||||
lines.append(c(trait.help.split("\n", 1)[0]))
|
||||
lines.append(f"# See also: {defining_class.__name__}.{name}")
|
||||
|
||||
lines.append(f"# c.{cls.__name__}.{name} = {default_repr}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
@classmethod
|
||||
def class_config_rst_doc(cls) -> str:
|
||||
"""Generate rST documentation for this class' config options.
|
||||
|
||||
Excludes traits defined on parent classes.
|
||||
"""
|
||||
lines = []
|
||||
classname = cls.__name__
|
||||
for _, trait in sorted(cls.class_traits(config=True).items()):
|
||||
ttype = trait.__class__.__name__
|
||||
|
||||
if not trait.name:
|
||||
continue
|
||||
termline = classname + "." + trait.name
|
||||
|
||||
# Choices or type
|
||||
if "Enum" in ttype:
|
||||
# include Enum choices
|
||||
termline += " : " + trait.info_rst() # type:ignore[attr-defined]
|
||||
else:
|
||||
termline += " : " + ttype
|
||||
lines.append(termline)
|
||||
|
||||
# Default value
|
||||
try:
|
||||
dvr = trait.default_value_repr()
|
||||
except Exception:
|
||||
dvr = None # ignore defaults we can't construct
|
||||
if dvr is not None:
|
||||
if len(dvr) > 64:
|
||||
dvr = dvr[:61] + "..."
|
||||
# Double up backslashes, so they get to the rendered docs
|
||||
dvr = dvr.replace("\\n", "\\\\n")
|
||||
lines.append(indent("Default: ``%s``" % dvr))
|
||||
lines.append("")
|
||||
|
||||
help = trait.help or "No description"
|
||||
lines.append(indent(dedent(help)))
|
||||
|
||||
# Blank line
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class LoggingConfigurable(Configurable):
|
||||
"""A parent class for Configurables that log.
|
||||
|
||||
Subclasses have a log trait, and the default behavior
|
||||
is to get the logger from the currently running Application.
|
||||
"""
|
||||
|
||||
log = Any(help="Logger or LoggerAdapter instance", allow_none=False)
|
||||
|
||||
@validate("log")
|
||||
def _validate_log(self, proposal: Bunch) -> LoggerType:
|
||||
if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)):
|
||||
# warn about unsupported type, but be lenient to allow for duck typing
|
||||
warnings.warn(
|
||||
f"{self.__class__.__name__}.log should be a Logger or LoggerAdapter,"
|
||||
f" got {proposal.value}.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return t.cast(LoggerType, proposal.value)
|
||||
|
||||
@default("log")
|
||||
def _log_default(self) -> LoggerType:
|
||||
if isinstance(self.parent, LoggingConfigurable):
|
||||
assert self.parent is not None
|
||||
return t.cast(logging.Logger, self.parent.log)
|
||||
from traitlets import log
|
||||
|
||||
return log.get_logger()
|
||||
|
||||
def _get_log_handler(self) -> logging.Handler | None:
|
||||
"""Return the default Handler
|
||||
|
||||
Returns None if none can be found
|
||||
|
||||
Deprecated, this now returns the first log handler which may or may
|
||||
not be the default one.
|
||||
"""
|
||||
if not self.log:
|
||||
return None
|
||||
logger: logging.Logger = (
|
||||
self.log if isinstance(self.log, logging.Logger) else self.log.logger
|
||||
)
|
||||
if not getattr(logger, "handlers", None):
|
||||
# no handlers attribute or empty handlers list
|
||||
return None
|
||||
return logger.handlers[0]
|
||||
|
||||
|
||||
CT = t.TypeVar("CT", bound="SingletonConfigurable")
|
||||
|
||||
|
||||
class SingletonConfigurable(LoggingConfigurable):
|
||||
"""A configurable that only allows one instance.
|
||||
|
||||
This class is for classes that should only have one instance of itself
|
||||
or *any* subclass. To create and retrieve such a class use the
|
||||
:meth:`SingletonConfigurable.instance` method.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def _walk_mro(cls) -> t.Generator[type[SingletonConfigurable], None, None]:
|
||||
"""Walk the cls.mro() for parent classes that are also singletons
|
||||
|
||||
For use in instance()
|
||||
"""
|
||||
|
||||
for subclass in cls.mro():
|
||||
if (
|
||||
issubclass(cls, subclass)
|
||||
and issubclass(subclass, SingletonConfigurable)
|
||||
and subclass != SingletonConfigurable
|
||||
):
|
||||
yield subclass
|
||||
|
||||
@classmethod
|
||||
def clear_instance(cls) -> None:
|
||||
"""unset _instance for this class and singleton parents."""
|
||||
if not cls.initialized():
|
||||
return
|
||||
for subclass in cls._walk_mro():
|
||||
if isinstance(subclass._instance, cls):
|
||||
# only clear instances that are instances
|
||||
# of the calling class
|
||||
subclass._instance = None # type:ignore[unreachable]
|
||||
|
||||
@classmethod
|
||||
def instance(cls: type[CT], *args: t.Any, **kwargs: t.Any) -> CT:
|
||||
"""Returns a global instance of this class.
|
||||
|
||||
This method create a new instance if none have previously been created
|
||||
and returns a previously created instance is one already exists.
|
||||
|
||||
The arguments and keyword arguments passed to this method are passed
|
||||
on to the :meth:`__init__` method of the class upon instantiation.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create a singleton class using instance, and retrieve it::
|
||||
|
||||
>>> from traitlets.config.configurable import SingletonConfigurable
|
||||
>>> class Foo(SingletonConfigurable): pass
|
||||
>>> foo = Foo.instance()
|
||||
>>> foo == Foo.instance()
|
||||
True
|
||||
|
||||
Create a subclass that is retrieved using the base class instance::
|
||||
|
||||
>>> class Bar(SingletonConfigurable): pass
|
||||
>>> class Bam(Bar): pass
|
||||
>>> bam = Bam.instance()
|
||||
>>> bam == Bar.instance()
|
||||
True
|
||||
"""
|
||||
# Create and save the instance
|
||||
if cls._instance is None:
|
||||
inst = cls(*args, **kwargs)
|
||||
# Now make sure that the instance will also be returned by
|
||||
# parent classes' _instance attribute.
|
||||
for subclass in cls._walk_mro():
|
||||
subclass._instance = inst
|
||||
|
||||
if isinstance(cls._instance, cls):
|
||||
return cls._instance
|
||||
else:
|
||||
raise MultipleInstanceError(
|
||||
f"An incompatible sibling of '{cls.__name__}' is already instantiated"
|
||||
f" as singleton: {type(cls._instance).__name__}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def initialized(cls) -> bool:
|
||||
"""Has an instance been created?"""
|
||||
return hasattr(cls, "_instance") and cls._instance is not None
|
1179
.venv/lib/python3.12/site-packages/traitlets/config/loader.py
Normal file
1179
.venv/lib/python3.12/site-packages/traitlets/config/loader.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
"""Manager to read and modify config data in JSON files.
|
||||
"""
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from traitlets.config import LoggingConfigurable
|
||||
from traitlets.traitlets import Unicode
|
||||
|
||||
|
||||
def recursive_update(target: dict[Any, Any], new: dict[Any, Any]) -> None:
|
||||
"""Recursively update one dictionary using another.
|
||||
|
||||
None values will delete their keys.
|
||||
"""
|
||||
for k, v in new.items():
|
||||
if isinstance(v, dict):
|
||||
if k not in target:
|
||||
target[k] = {}
|
||||
recursive_update(target[k], v)
|
||||
if not target[k]:
|
||||
# Prune empty subdicts
|
||||
del target[k]
|
||||
|
||||
elif v is None:
|
||||
target.pop(k, None)
|
||||
|
||||
else:
|
||||
target[k] = v
|
||||
|
||||
|
||||
class BaseJSONConfigManager(LoggingConfigurable):
|
||||
"""General JSON config manager
|
||||
|
||||
Deals with persisting/storing config in a json file
|
||||
"""
|
||||
|
||||
config_dir = Unicode(".")
|
||||
|
||||
def ensure_config_dir_exists(self) -> None:
|
||||
try:
|
||||
os.makedirs(self.config_dir, 0o755)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def file_name(self, section_name: str) -> str:
|
||||
return os.path.join(self.config_dir, section_name + ".json")
|
||||
|
||||
def get(self, section_name: str) -> Any:
|
||||
"""Retrieve the config data for the specified section.
|
||||
|
||||
Returns the data as a dictionary, or an empty dictionary if the file
|
||||
doesn't exist.
|
||||
"""
|
||||
filename = self.file_name(section_name)
|
||||
if os.path.isfile(filename):
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def set(self, section_name: str, data: Any) -> None:
|
||||
"""Store the given config data."""
|
||||
filename = self.file_name(section_name)
|
||||
self.ensure_config_dir_exists()
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def update(self, section_name: str, new_data: Any) -> Any:
|
||||
"""Modify the config section by recursively updating it with new_data.
|
||||
|
||||
Returns the modified config data as a dictionary.
|
||||
"""
|
||||
data = self.get(section_name)
|
||||
recursive_update(data, new_data)
|
||||
self.set(section_name, data)
|
||||
return data
|
164
.venv/lib/python3.12/site-packages/traitlets/config/sphinxdoc.py
Normal file
164
.venv/lib/python3.12/site-packages/traitlets/config/sphinxdoc.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Machinery for documenting traitlets config options with Sphinx.
|
||||
|
||||
This includes:
|
||||
|
||||
- A Sphinx extension defining directives and roles for config options.
|
||||
- A function to generate an rst file given an Application instance.
|
||||
|
||||
To make this documentation, first set this module as an extension in Sphinx's
|
||||
conf.py::
|
||||
|
||||
extensions = [
|
||||
# ...
|
||||
'traitlets.config.sphinxdoc',
|
||||
]
|
||||
|
||||
Autogenerate the config documentation by running code like this before
|
||||
Sphinx builds::
|
||||
|
||||
from traitlets.config.sphinxdoc import write_doc
|
||||
from myapp import MyApplication
|
||||
|
||||
writedoc('config/options.rst', # File to write
|
||||
'MyApp config options', # Title
|
||||
MyApplication()
|
||||
)
|
||||
|
||||
The generated rST syntax looks like this::
|
||||
|
||||
.. configtrait:: Application.log_datefmt
|
||||
|
||||
Description goes here.
|
||||
|
||||
Cross reference like this: :configtrait:`Application.log_datefmt`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
from collections import defaultdict
|
||||
from textwrap import dedent
|
||||
|
||||
from traitlets import HasTraits, Undefined
|
||||
from traitlets.config.application import Application
|
||||
from traitlets.utils.text import indent
|
||||
|
||||
|
||||
def setup(app: t.Any) -> dict[str, t.Any]:
|
||||
"""Registers the Sphinx extension.
|
||||
|
||||
You shouldn't need to call this directly; configure Sphinx to use this
|
||||
module instead.
|
||||
"""
|
||||
app.add_object_type("configtrait", "configtrait", objname="Config option")
|
||||
return {"parallel_read_safe": True, "parallel_write_safe": True}
|
||||
|
||||
|
||||
def interesting_default_value(dv: t.Any) -> bool:
|
||||
if (dv is None) or (dv is Undefined):
|
||||
return False
|
||||
if isinstance(dv, (str, list, tuple, dict, set)):
|
||||
return bool(dv)
|
||||
return True
|
||||
|
||||
|
||||
def format_aliases(aliases: list[str]) -> str:
|
||||
fmted = []
|
||||
for a in aliases:
|
||||
dashes = "-" if len(a) == 1 else "--"
|
||||
fmted.append(f"``{dashes}{a}``")
|
||||
return ", ".join(fmted)
|
||||
|
||||
|
||||
def class_config_rst_doc(cls: type[HasTraits], trait_aliases: dict[str, t.Any]) -> str:
|
||||
"""Generate rST documentation for this class' config options.
|
||||
|
||||
Excludes traits defined on parent classes.
|
||||
"""
|
||||
lines = []
|
||||
classname = cls.__name__
|
||||
for _, trait in sorted(cls.class_traits(config=True).items()):
|
||||
ttype = trait.__class__.__name__
|
||||
|
||||
fullname = classname + "." + (trait.name or "")
|
||||
lines += [".. configtrait:: " + fullname, ""]
|
||||
|
||||
help = trait.help.rstrip() or "No description"
|
||||
lines.append(indent(dedent(help)) + "\n")
|
||||
|
||||
# Choices or type
|
||||
if "Enum" in ttype:
|
||||
# include Enum choices
|
||||
lines.append(indent(":options: " + ", ".join("``%r``" % x for x in trait.values))) # type:ignore[attr-defined]
|
||||
else:
|
||||
lines.append(indent(":trait type: " + ttype))
|
||||
|
||||
# Default value
|
||||
# Ignore boring default values like None, [] or ''
|
||||
if interesting_default_value(trait.default_value):
|
||||
try:
|
||||
dvr = trait.default_value_repr()
|
||||
except Exception:
|
||||
dvr = None # ignore defaults we can't construct
|
||||
if dvr is not None:
|
||||
if len(dvr) > 64:
|
||||
dvr = dvr[:61] + "..."
|
||||
# Double up backslashes, so they get to the rendered docs
|
||||
dvr = dvr.replace("\\n", "\\\\n")
|
||||
lines.append(indent(":default: ``%s``" % dvr))
|
||||
|
||||
# Command line aliases
|
||||
if trait_aliases[fullname]:
|
||||
fmt_aliases = format_aliases(trait_aliases[fullname])
|
||||
lines.append(indent(":CLI option: " + fmt_aliases))
|
||||
|
||||
# Blank line
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def reverse_aliases(app: Application) -> dict[str, list[str]]:
|
||||
"""Produce a mapping of trait names to lists of command line aliases."""
|
||||
res = defaultdict(list)
|
||||
for alias, trait in app.aliases.items():
|
||||
res[trait].append(alias)
|
||||
|
||||
# Flags also often act as aliases for a boolean trait.
|
||||
# Treat flags which set one trait to True as aliases.
|
||||
for flag, (cfg, _) in app.flags.items():
|
||||
if len(cfg) == 1:
|
||||
classname = next(iter(cfg))
|
||||
cls_cfg = cfg[classname]
|
||||
if len(cls_cfg) == 1:
|
||||
traitname = next(iter(cls_cfg))
|
||||
if cls_cfg[traitname] is True:
|
||||
res[classname + "." + traitname].append(flag)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def write_doc(path: str, title: str, app: Application, preamble: str | None = None) -> None:
|
||||
"""Write a rst file documenting config options for a traitlets application.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The file to be written
|
||||
title : str
|
||||
The human-readable title of the document
|
||||
app : traitlets.config.Application
|
||||
An instance of the application class to be documented
|
||||
preamble : str
|
||||
Extra text to add just after the title (optional)
|
||||
"""
|
||||
trait_aliases = reverse_aliases(app)
|
||||
with open(path, "w") as f:
|
||||
f.write(title + "\n")
|
||||
f.write(("=" * len(title)) + "\n")
|
||||
f.write("\n")
|
||||
if preamble is not None:
|
||||
f.write(preamble + "\n\n")
|
||||
|
||||
for c in app._classes_inc_parents():
|
||||
f.write(class_config_rst_doc(c, trait_aliases))
|
||||
f.write("\n")
|
Reference in New Issue
Block a user