fixed subscription table

This commit is contained in:
2025-02-02 00:02:31 -05:00
parent a1ab31acfe
commit ef5f57e678
5389 changed files with 686710 additions and 28 deletions

View File

@@ -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",
]

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View 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")