Source code for click_extra.context

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Click context plumbing: the {class}`Context` subclass plus the central
registry of every `ctx.meta` key Click Extra writes or reads.

Click's {attr}`click.Context.meta` is a per-invocation dict that Click shares
across the parent/child context hierarchy. Click Extra uses it to pass
per-invocation state (the picked theme, the resolved table format, the loaded
configuration, etc.) between eager callbacks and the rest of the CLI without
mutating module-level globals. Per-invocation context storage is what keeps
back-to-back invocations of the same CLI (Sphinx builds, test runners, REPLs)
from leaking state into each other.

This module is part of Click Extra's **public API**. Inside any
`@command`- or `@group`-decorated function, request the active context
with {func}`click.pass_context` (or call {func}`click.get_current_context`)
and read the entries you need:

```{code-block} python

from click_extra import command, context, echo, pass_context


@command
@pass_context
def cli(ctx):
    echo(f"Theme: {ctx.meta[context.THEME]}")
    echo(f"Jobs:  {ctx.meta[context.JOBS]}")
```

Each constant below documents who writes the entry, when, and what shape the
value takes. The raw string values are stable and downstream code may also
read `ctx.meta["click_extra.<field>"]` directly: the constants exist so
internal call sites and downstream code can converge on a single spelling.
"""

from __future__ import annotations

import functools
import os
import sys
import weakref
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from gettext import gettext as _
from typing import Any, ParamSpec, TypeVar, cast

import click
import cloup
from click import echo
from click.exceptions import Abort, Exit

from ._utils import exception_chain
from .color import resolve_color_env
from .highlight import HelpFormatter

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Callable, Generator, Sequence
    from types import TracebackType
    from typing import Concatenate, Final

    from .table import TableFormat, THeader

P = ParamSpec("P")
R = TypeVar("R")


POSIXLY_CORRECT_ENVVAR: Final[str] = "POSIXLY_CORRECT"
"""Environment variable that requests strict POSIX argument parsing.

When this variable is present in the environment (regardless of its value,
matching GNU `getopt` semantics), {class}`Context` forces
`allow_interspersed_args` to `False` so option parsing stops at the first
positional argument.
"""


_ABORT_EXCEPTIONS: Final = (EOFError, KeyboardInterrupt, Abort)
"""The exceptions Click's `main()` answers with `Aborted!` and the exit status `1`."""


@dataclass
class _AbortNotice:
    """The `Aborted!` notice of one standalone run of a Click Extra command.

    Click prints that notice once the root context has closed, so after every
    close callback of the run. A run that owns its notice prints it as soon as
    the abort leaves a command, before any context closes: the user reads it at
    once, even when the clean-up is slow.
    """

    command: click.Command
    """The command whose `main()` drives the run."""

    printed_for: BaseException | None = None
    """The abort the notice is printed for, if any."""

    def covers(self, exc: BaseException | None) -> bool:
        """Whether the notice is printed for `exc`, or for the abort `exc` stems from.

        An abort keeps its notice on its way up the commands, and when a later
        exception replaces it, like a second Ctrl+C landing in a close callback.
        """
        return any(link is self.printed_for for link in exception_chain(exc))


_ABORT_NOTICE: ContextVar[_AbortNotice | None] = ContextVar(
    "_ABORT_NOTICE", default=None
)
"""The notice of the run in progress, or `None` where Click prints its own."""


@contextmanager
def _owning_abort_notice(
    command: click.Command, standalone: bool
) -> Generator[None, None, None]:
    """Make the run of `command` in the block print its own abort notice.

    Only a standalone run prints one. Without standalone mode, Click raises the
    {exc}`~click.exceptions.Abort` to the caller and prints no notice.
    """
    token = _ABORT_NOTICE.set(_AbortNotice(command) if standalone else None)
    try:
        yield
    finally:
        _ABORT_NOTICE.reset(token)


@contextmanager
def _abort_notice(ctx: click.Context) -> Generator[None, None, None]:
    """Print Click's `Aborted!` as soon as an abort leaves the block.

    Wraps the invocation of a command, which Click runs inside the context of
    that command. The notice so precedes the close callbacks of every context of
    the run, the innermost included.

    Code that runs a command through {meth}`click.Context.invoke` never enters
    this block, so it stays free to catch the abort and carry on.
    """
    try:
        yield
    except _ABORT_EXCEPTIONS as exc:
        notice = _ABORT_NOTICE.get()
        if (
            notice is not None
            and ctx.find_root().command is notice.command
            and not notice.covers(exc)
        ):
            notice.printed_for = exc
            if not isinstance(exc, Abort):
                # The blank line ends the line of the `^C` the terminal echoed.
                echo(file=sys.stderr)
            echo(_("Aborted!"), file=sys.stderr)
        raise


_RUN_CLEANUPS: ContextVar[list[Callable[[], None]] | None] = ContextVar(
    "_RUN_CLEANUPS", default=None
)
"""What the runs started within the `_scoped_run` block in progress may leave.

One callable for each piece of process-wide state a run took: its hold on the
handler of the `root` logger, the log levels it set, the color it published.
`None` outside such a block. Written by `_queue_run_cleanup`.
"""


def _queue_run_cleanup(ctx: click.Context, cleanup: Callable[[], None]) -> None:
    """Have `cleanup` called for the run of `ctx`, even when Click never closes it.

    A run that fails while parsing leaves its context never entered, and therefore
    never closed: its close callbacks do not run. Two mechanisms cover that path:

    - A finalizer, called when Python collects the context. That may be late, and
      in the middle of a later run.
    - The `_scoped_run` block {meth}`click_extra.commands.Command.main` runs
      within, which does not wait for a collection.

    Both also call `cleanup` for a context that did close. It so has to do nothing
    when called again, and to leave alone what a later run took over. The options
    taking process-wide state queue their cleanup here beside the close callback
    they register for the normal path: see `click_extra.logging` and
    {func}`click_extra.color.publish_invocation_color`.
    """
    weakref.finalize(ctx, cleanup)
    cleanups = _RUN_CLEANUPS.get()
    if cleanups is not None:
        cleanups.append(cleanup)


@contextmanager
def _scoped_run() -> Generator[None, None, None]:
    """Give back on exit the process-wide state of the runs started within the block.

    {meth}`click_extra.commands.Command.main` runs within it. A run that fails
    while parsing so gives back the handler of the `root` logger, resets the
    levels of its loggers and clears the color it published, without waiting for
    a garbage collection. What the run around the block took stays: that run
    gives it back when it ends.
    """
    cleanups: list[Callable[[], None]] = []
    reset_token = _RUN_CLEANUPS.set(cleanups)
    try:
        yield
    finally:
        _RUN_CLEANUPS.reset(reset_token)
        for cleanup in cleanups:
            cleanup()


[docs] class Context(cloup.Context): """Like `cloup._context.Context`, but with the ability to populate the context's `meta` property at instantiation. Also defaults `color` to `True` for root contexts (those without a parent), so help screens are always colorized, even when piped. Click's own default is `None` (auto-detect via TTY), which strips colors in non-interactive contexts. Parent-to-child color inheritance is handled by Click itself at `Context.__init__` time, so no property override is needed. When the `POSIXLY_CORRECT` environment variable is set, this context forces `allow_interspersed_args` to `False` so option parsing stops at the first positional argument, as GNU getopt-based tools do. See {data}`~click_extra.context.POSIXLY_CORRECT_ENVVAR`. Carries the `sort_subcommands` setting read by {meth}`click_extra.commands.Group.must_sort_subcommands`, so a whole command tree can settle its subcommand listings in one place. ```{todo} Propose addition of `meta` keyword upstream to Click. ``` """ formatter_class = HelpFormatter """Use our own formatter to colorize the help screen.""" def __init__( self, *args, meta: dict[str, Any] | None = None, sort_subcommands: bool | None = None, **kwargs, ) -> None: """Like parent's context but with an extra `meta` keyword-argument. Also pre-seed `color` from the color environment variables for a parentless context when the user did not provide it, and force `allow_interspersed_args` to `False` when `POSIXLY_CORRECT` is set in the environment. :param sort_subcommands: whether groups list their subcommands alphabetically. Inherited from the parent context when left to `None`, the way Cloup inherits `align_sections`, so declaring it once on the root group settles every subgroup below it. """ super().__init__(*args, **kwargs) # Inherit from the parent context, so a setting declared on the root group # reaches subgroups that never mention it. Cloup does the same for its own # `align_sections` and `show_subcommand_aliases` settings. self.sort_subcommands = ( sort_subcommands if sort_subcommands is not None else getattr(self.parent, "sort_subcommands", None) ) # Click defaults root `ctx.color` to `None` (GNU `auto`: keep ANSI on a # TTY, strip it when piped). For a parentless context, pre-seed it from the # color environment variables so the eager help and version screens, which # can render before `--color` resolves, still honor `FORCE_COLOR` / # `NO_COLOR`. With no recognized variable the value stays `None` (auto), # and the `ColorOption` callback later layers the command line, configuration # and `--accessible` on top. if not self.parent and self.color is None: self.color = resolve_color_env() # Honor the POSIX conformance switch: when POSIXLY_CORRECT is present in the # environment, stop parsing options at the first positional argument, the way # GNU getopt-based tools do. Presence alone is enough, regardless of value. # This can only tighten parsing (True -> False), never loosen it. if POSIXLY_CORRECT_ENVVAR in os.environ: self.allow_interspersed_args = False # Update the context's meta property with the one provided by user. if meta: self._meta.update(meta) def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, tb: TracebackType | None, ) -> bool | None: """Like parent's `__exit__`, but ends an aborted run whose notice is printed. {func}`_abort_notice` prints `Aborted!` before the contexts of the run close. Click would print it again once the root context has closed, so that context exits with Click's abort status itself. """ notice = _ABORT_NOTICE.get() # Only the exit that closes the root context ends the run. if ( notice is None or notice.printed_for is None or self._depth != 1 or self.parent is not None or self.command is not notice.command ): return super().__exit__(exc_type, exc_value, tb) try: suppressed = super().__exit__(exc_type, exc_value, tb) except _ABORT_EXCEPTIONS as exc: # A close callback raised an abort, like a second Ctrl+C landing there. if not notice.covers(exc): raise raise Exit(1) from exc if suppressed or not ( isinstance(exc_value, _ABORT_EXCEPTIONS) and notice.covers(exc_value) ): return suppressed raise Exit(1) from exc_value
[docs] def render_table( self, table_data: Sequence[Sequence[str | None]], headers: Sequence[THeader] | None = None, table_format: TableFormat | None = None, sort_key: Callable[[Sequence[str | None]], Any] | None = None, **kwargs: Any, ) -> str: """Render a table honoring the invocation's table options, and return it. Same contract as {func}`click_extra.table.render_table`, with `table_format` and `sort_key` defaulting to the values resolved by the `--table-format` and `--sort-by` options (read from {data}`~click_extra.context.TABLE_FORMAT` and {data}`~click_extra.context.TABLE_SORT_KEY`). `ctx.meta` is shared along the context chain, so this works from any subcommand without reaching for the root context. Without those options in the chain, falls back to the default format and no sort. """ # Imported here because the table module depends on this one. from .table import render_table if table_format is None: table_format = get(self, TABLE_FORMAT) if sort_key is None: sort_key = get(self, TABLE_SORT_KEY) return render_table( table_data, headers, table_format=table_format, sort_key=sort_key, **kwargs, )
[docs] def print_table( self, table_data: Sequence[Sequence[str | None]], headers: Sequence[THeader] | None = None, table_format: TableFormat | None = None, sort_key: Callable[[Sequence[str | None]], Any] | None = None, **kwargs: Any, ) -> None: """Render a table honoring the invocation's table options, and print it. The printing counterpart of {meth}`render_table`: same defaulting of `table_format` and `sort_key` from the context's shared `meta`, delegating to {func}`click_extra.table.print_table` (which also handles ANSI translation and colorization policy). """ # Imported here because the table module depends on this one. from .table import print_table if table_format is None: table_format = get(self, TABLE_FORMAT) if sort_key is None: sort_key = get(self, TABLE_SORT_KEY) print_table( table_data, headers, table_format=table_format, sort_key=sort_key, **kwargs, )
META_NAMESPACE: Final[str] = "click_extra." """Prefix shared by every `ctx.meta` key Click Extra writes. Reserved for entries the framework owns. Downstream consumers picking their own `ctx.meta` keys are encouraged to use a different prefix to avoid colliding with current or future Click Extra entries. """ # A real dict, not a UserDict: Click types `ctx._meta` as one and reads it directly. class _LazyMetaDict(dict): # noqa: FURB189 """Dict subclass that lazily resolves fields on first access. Installed as `ctx._meta` so that `ctx.meta["click_extra.<field>"]` transparently evaluates the corresponding `@cached_property` on the source object only when the key is actually read. """ def __init__( self, base: dict[str, Any], source: object, fields: tuple[str, ...], ) -> None: super().__init__(base) self._source = source self._lazy_keys = {f"{META_NAMESPACE}{f}": f for f in fields} def _resolve(self, key: str) -> Any: """Resolve a lazy key, cache the result, and return it.""" value = getattr(self._source, self._lazy_keys[key]) # Store as a regular entry so subsequent reads are plain dict lookups. self[key] = value return value def __getitem__(self, key: str) -> Any: if key in self._lazy_keys and not dict.__contains__(self, key): return self._resolve(key) return super().__getitem__(key) def __contains__(self, key: object) -> bool: return key in self._lazy_keys or super().__contains__(key) def get(self, key: str, default: Any = None) -> Any: if key in self._lazy_keys: # The dict's own test, since `__contains__` above claims every lazy key. if dict.__contains__(self, key): # noqa: PLC2801 return super().__getitem__(key) return self._resolve(key) return super().get(key, default) # --- Argument capture --------------------------------------------------------- RAW_ARGS: Final[str] = "click_extra.raw_args" """The raw, pre-parsed `argv` slice fed to the current command. Written by {class}`click_extra.commands.Command.make_context` so that {class}`click_extra.parameters.ShowParamsOption` can re-parse the original arguments for the `--params` table without re-running the callbacks. Consumers normalize the parser's `UNSET` sentinel back to `None` on read, matching what `click.Command.parse_args` does for `ctx.params`. ```{todo} Open a narrowly-scoped Click issue and PR for a public {class}`click.Context` accessor returning a parameter's resolved `(value, ParameterSource)` after parsing, without re-firing eager callbacks. That is the forward resolution `--params` actually consumes, and landing it retires the re-parse this constant exists to feed. Reference [`pallets/click#1279`](https://github.com/pallets/click/issues/1279) as related, not as home: it asks for the inverse direction (reconstructing a normalized argv from a Context), was scoped with about a dozen normalization rules, flagged underdefined, and has stalled since 2023. Tracked in `docs/upstream.md` under "Normalized arguments"; the developer note below records why the cheaper alternatives do not work. ``` """ # Developer note: why RAW_ARGS exists, and what to actually propose upstream. # # What --params needs is the *forward* resolution: for every parameter, the # value it resolves to and its provenance (ParameterSource), computable at an # arbitrary moment. Click does not expose this: # - The parsed command-line values (`opts`) are a local in # `click.Command.parse_args`, discarded when it returns. They are never # stored on the Context, despite the convenience of pretending otherwise. # - --params is eager, so its callback fires mid-processing, before the # non-eager parameters land in `ctx.params`. Reading `ctx.params` there would # see a partial picture. # # Workaround: Command/Group.make_context stashes a copy of the raw # argv under RAW_ARGS; parameters.render_params_table rebuilds the parser, # re-parses those args to recover `opts`, and calls `consume_value()` (not # `handle_parse_result()`) per parameter so eager callbacks are not re-fired. # The same re-parse backs `click-extra wrap --params`, which introspects a # foreign CLI that is never actually executed (no live parse to borrow from). # # Fragilities to keep in mind: # - The re-parse replays the parser but skips Click's post-parse cleanup, so it # drifts whenever that cleanup changes. Click 8.4 introduced the UNSET # sentinel (returned for parameters with no default); parse_args rewrites it # to None, the re-parse did not, and the sentinel leaked into the value and # default columns. It is now normalized back to None at the consumer sites # (render_params_table and format_param_row). # - It depends on `Command.make_parser()` handing back # `click.parser._OptionParser`, private since the Click 8.2 parser rework. # # Upstream proposal (the actionable half is the `{todo}` on RAW_ARGS above). # Three different features get muddled under the "raw_args" label: # 1. Preserve the raw input argv on the Context. Trivial; what we do here. # 2. Expose the parsed `opts`, or better, a per-parameter resolved # (value, ParameterSource), on the Context. Modest; this is what # --params actually consumes. # 3. Reconstruct a *normalized* argv from a Context (the inverse direction). # Hard and underdefined; this is click#1279. # click#1279 was filed for pip-tools and is feature 3: davidism scoped it with # ~a dozen normalization rules (option ordering, prompt/env inclusion, default # exclusion, ambiguous file paths) and flagged it underdefined; it has stalled # since 2023. click-extra needs feature 2, the easier forward direction, so the # right move is a new, narrowly-scoped issue/PR for a public Context accessor # returning a parameter's resolved (value, ParameterSource) after parsing # without re-firing eager callbacks, referencing click#1279 as related, not home. # # Note: "just reuse the `opts` lying around" does not work as stated. `opts` is # not on the Context; capturing it means reimplementing parse_args or wrapping # make_parser, it would still need the UNSET normalization above, and it would # not help the foreign-CLI wrap path, which has no live parse to capture from. # --- Invocation name ---------------------------------------------------------- INVOCATION_NAME: Final[str] = "click_extra.invocation_name" """The name the CLI was invoked under, as resolved on the root context. Written by {class}`click_extra.commands.Command.make_context` on the root context only (`ctx.meta` is shared down the whole hierarchy, so a subcommand must not overwrite it). Equals Click's root `info_name`: the entry-point script name, the symlink name in {mod}`~click_extra.multicall` personality mode, or an explicit `prog_name` override passed to `main()`. Read it with {func}`click.pass_context` to implement invocation-name-aware behavior without the full multicall machinery: ```{code-block} python if ctx.meta[context.INVOCATION_NAME] == "bunzip2": ... ``` """ # --- Configuration loading ---------------------------------------------------- CONF_SOURCE: Final[str] = "click_extra.conf_source" """Resolved path or URL of the configuration file that was loaded. Written by {class}`click_extra.config.option.ConfigOption.load_conf` after a configuration file is found and parsed. `None` if no file matched. """ CONF_FULL: Final[str] = "click_extra.conf_full" """Full parsed configuration document (the whole file, every section). Written by {class}`click_extra.config.option.ConfigOption.load_conf`. Read by {class}`click_extra.commands.Group` (for subcommand inheritance) and by {func}`click_extra.cli_wrapper.invoke_target` (to forward the loaded config to wrapped CLIs). With `cascade=True` this holds the deep-merged view of every loaded file, which is the configuration as it was actually applied. """ CONF_SOURCES: Final[str] = "click_extra.conf_sources" """Every configuration file loaded, in precedence order (highest first). Written by {class}`click_extra.config.option.ConfigOption.load_conf` as a (possibly empty) tuple of `(location, parsed_conf)` pairs. Holds a single entry in the default first-file-wins mode, and one entry per loaded file when `cascade=True`. """ TOOL_CONFIG: Final[str] = "click_extra.tool_config" """The app-specific config section, deserialized through `config_schema`. Written by {class}`~click_extra.config.option.ConfigOption`'s `_apply_config_schema` method only when a schema callable is configured. Read via {func}`click_extra.config.schema.get_tool_config`. """ SUBCOMMANDS_APPLIED: Final[str] = "click_extra.subcommands_applied" """Command paths whose reserved subcommand keys were already spliced in. Written by {func}`click_extra.config.subcommands.inject_reserved_subcommands` as a growing set of `ctx.command_path` strings. This `meta` dict is shared by the whole context tree, so the set is what tells one group's injection from another's, and what keeps a second pass over the same group from prepending its `_prepend_subcommands` twice. """ # --- Verbosity / logging ------------------------------------------------------ VERBOSITY_LEVEL: Final[str] = "click_extra.verbosity_level" """The reconciled {class}`~click_extra.logging.LogLevel` chosen for the run. Written by `_VerbosityOption.apply_verbosity`, which reconciles every verbosity-related option (`--verbosity`, `--verbose`/`-v` and `--quiet`/`-q`) into a single level. Read by the same method to detect whether the reconciled level changed since a sibling option last fired. """ VERBOSITY: Final[str] = "click_extra.verbosity" """Raw value of `--verbosity LEVEL` as the user passed it. Written by {meth}`click_extra.logging.VerbosityOption.set_level`. Stored alongside {data}`VERBOSITY_LEVEL` so downstream code can tell whether the final level came from `--verbosity` or from `-v`/`-q` repetitions. """ DEBUG: Final[str] = "click_extra.debug" """Whether `--debug` was passed. Written by the `set_level` callback of {class}`click_extra.logging.DebugOption`. Read by `_VerbosityOption.resolve_level`, which answers it before anything else: `--debug` names the loudest level there is, so nothing another option asks for can outrank it. """ VERBOSE: Final[str] = "click_extra.verbose" """Raw repetition count of `--verbose`/`-v`. Written by the `set_level` callback of {class}`click_extra.logging.VerboseOption`. Combined with {data}`QUIET` into the signed `verbose - quiet` counter that `_VerbosityOption.resolve_level` shifts the base level by. """ QUIET: Final[str] = "click_extra.quiet" """Raw repetition count of `--quiet`/`-q`. Written by the `set_level` callback of {class}`click_extra.logging.QuietOption`. The quiet counterpart of {data}`VERBOSE`: each `-q` subtracts one step from the `verbose - quiet` net applied on top of the base verbosity level. """ # --- Timing ------------------------------------------------------------------- START_TIME: Final[str] = "click_extra.start_time" """`time.perf_counter()` snapshot taken when `--time` is enabled. Written by {class}`click_extra.execution.TimerOption.init_timer`. """ # --- Parallelism -------------------------------------------------------------- JOBS: Final[str] = "click_extra.jobs" """Effective parallel job count after clamping (always >= 1). Written by {class}`click_extra.execution.JobsOption.validate_jobs`. Click Extra itself does not act on this value: it is a contract for downstream commands that drive their own concurrency. """ # --- Table rendering ---------------------------------------------------------- TABLE_FORMAT: Final[str] = "click_extra.table_format" """The {class}`~click_extra.table.TableFormat` chosen via `--table-format`. Written by {class}`click_extra.table.TableFormatOption.init_formatter`. Read by {meth}`Context.render_table` and {meth}`Context.print_table` as their default format. """ SORT_BY: Final[str] = "click_extra.sort_by" """Tuple of column IDs picked via `--sort-by` (in priority order). Written by {class}`click_extra.table.SortByOption.init_sort`. """ TABLE_SORT_KEY: Final[str] = "click_extra.table_sort_key" """Row sort key derived from the `--sort-by` selection. Written by {class}`click_extra.table.SortByOption.init_sort` when its column definitions are known at declaration time (labeled mode). Read by {meth}`Context.render_table` and {meth}`Context.print_table` as their default `sort_key`. Absent in field-vocabulary mode, where the sort is resolved per table by {func}`click_extra.table.print_table` from the column IDs its headers carry. """ COLUMNS: Final[str] = "click_extra.columns" """Tuple of column IDs selected via `--columns` (in display order). Written by {class}`click_extra.table.ColumnsOption.init_columns`. Read by table-rendering consumers (like {class}`click_extra.parameters.ShowParamsOption`) to project and reorder columns before emitting the table. Empty / unset means no projection: render every column in its canonical order. """ # --- Theming ------------------------------------------------------------------ THEME: Final[str] = "click_extra.theme.active" """The {class}`~click_extra.theme.HelpTheme` active for this invocation. Written by {class}`click_extra.theme.ThemeOption.set_theme`. Read via {func}`click_extra.theme.get_current_theme`, which falls back to `click_extra.theme.default_theme` when no key is set. """ THEME_OVERRIDES: Final[str] = "click_extra.theme.overrides" """Per-invocation theme registry overlay loaded from the user's config file. Written by {class}`click_extra.config.option.ConfigOption` when it sees `[tool.<cli>.themes.<name>]` tables: each table is built into a {class}`~click_extra.theme.HelpTheme` (cascading on top of an existing theme when *name* matches one already in {data}`~click_extra.theme.theme_registry`). Read by {func}`click_extra.theme.get_theme_registry` so `--theme` can pick the new themes without leaking them into sibling invocations sharing the same process. """ # --- Telemetry ---------------------------------------------------------------- TELEMETRY: Final[str] = "click_extra.telemetry" """`True` if the user opted into telemetry, `False` otherwise. Written by {class}`click_extra.telemetry.TelemetryOption.set_telemetry` after reconciling `--telemetry` / `--no-telemetry` with the standard `DO_NOT_TRACK` environment variable. Downstream code reads this to decide whether to emit usage data. """ # --- Progress ----------------------------------------------------------------- PROGRESS: Final[str] = "click_extra.progress" """`True` when the CLI may display progress spinners, `False` otherwise. Written by {class}`click_extra.spinner.ProgressOption.set_progress` from the `--progress` / `--no-progress` flag (which `--accessible` lowers to `False`). {class}`~click_extra.spinner.Spinner`, {class}`~click_extra.spinner.OperationTrail` and {func}`~click_extra.spinner.progressbar` read it on their own when left at their automatic default. Deliberately independent of color: a spinner is an interactivity concern, so it is gated on the terminal (TTY / `TERM=dumb`, handled by the spinner) and on explicit intent (`--no-progress` / `--accessible`), never on `--no-color` / `NO_COLOR`. See {class}`~click_extra.spinner.ProgressOption` for the rationale. """ # --- Accessibility ------------------------------------------------------------ ACCESSIBLE: Final[str] = "click_extra.accessible" """`True` when the user requested screen-reader-friendly output. Written by {meth}`~click_extra.AccessibleOption.set_accessible` after reconciling the `--accessible` flag with the `ACCESSIBLE` environment variable. Read by output helpers that must degrade a cursor-driven element to a linear stream: {func}`~click_extra.clear` becomes a no-op and {func}`~click_extra.echo_via_pager` writes its text straight to stdout instead of spawning a pager. This is the *readable* counterpart to the `--color` / `--progress` / `--table-format` defaults that `--accessible` also lowers: those are consumed through their own resolved values (`ctx.color`, {data}`PROGRESS`, the table format), while this flag exposes the accessibility intent itself. """ # --- Exit code ---------------------------------------------------------------- ZERO_EXIT: Final[str] = "click_extra.zero_exit" """`True` when the user asked the CLI to always return a zero exit code. Written by {class}`click_extra.execution.ZeroExitOption.set_zero_exit`. Click Extra itself does not act on this value: it is a contract for downstream commands that suppress their non-zero "problems found" exit code and return `0` as long as the run itself succeeded. """ # --- Helpers ------------------------------------------------------------------
[docs] def pass_context(func: Callable[Concatenate[Context, P], R]) -> Callable[P, R]: """Mark a callback as wanting the active {class}`Context` as its first argument. Click's own {func}`click.pass_context` is typed for the base {class}`click.Context`. A handler annotated with click-extra's enhanced {class}`Context` (to reach its extra helpers like `ctx.print_table`) therefore fails static type checking: function parameters are contravariant, so `Callable[[Context], R]` is not assignable where a `Callable[[click.Context], R]` is expected. This drop-in is typed for the enhanced {class}`Context` and still accepts handlers typed for the base `click.Context` (a wider first parameter is allowed), so both type-check. At runtime it forwards the active context unchanged, exactly like {func}`click.pass_context`. """ @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return func(cast(Context, click.get_current_context()), *args, **kwargs) return wrapper
[docs] def get(ctx: click.Context, key: str, default: Any = None) -> Any: """Read `key` from the current context's shared `meta` dict. Equivalent to `ctx.meta.get(key, default)`. Click's `meta` is shared across the parent/child hierarchy, so reading from the local context is sufficient: there is no need to walk up to the root manually. """ return ctx.meta.get(key, default)
[docs] def set( ctx: click.Context, key: str, value: Any, ) -> None: """Write `value` under `key` in the current context's shared `meta` dict. Equivalent to `ctx.meta[key] = value`. Provided as the symmetric writer for {func}`get` so that callers can route both sides of a `meta` access through this module. """ ctx.meta[key] = value