asm
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.
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
asm/venv/lib/python3.11/site-packages/traitlets/config/loader.py
Normal file
1179
asm/venv/lib/python3.11/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
|
||||
@@ -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