# 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 central registry of every `ctx.meta` key Click
Extra writes or reads.
The {class}`~click_extra.commands.Context` subclass lives in
{mod}`click_extra.commands`, beside the commands that create it.
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 weakref
from collections.abc import ItemsView, KeysView, ValuesView
from contextlib import contextmanager
from contextvars import ContextVar
from functools import wraps
from typing import cast
import click
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Generator, Iterator
from typing import Any, Concatenate, Final, ParamSpec, TypeVar
from .commands import Context
P = ParamSpec("P")
R = TypeVar("R")
_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()
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.
A lazy key belongs to the mapping before it resolves: `in`, `len()`,
iteration and `keys()` count it without resolving it. What reads its value
resolves it: indexing, `get()`, `items()`, `values()`, `pop()`,
`setdefault()` and equality. A deleted lazy key stays deleted.
"""
def __init__(
self,
base: dict[str, Any],
source: object,
fields: tuple[str, ...],
) -> None:
# Copy only the entries `base` stores, so a lazy `base` keeps its pending
# keys unresolved: each `--version` option of a command tree wraps
# `ctx.meta` again.
super().__init__(dict.items(base))
self._source = source
self._lazy_keys = {f"{META_NAMESPACE}{f}": f for f in fields}
def _pending(self) -> list[str]:
"""The lazy keys not resolved yet, in the order they were declared."""
stored = dict.keys(self)
return [key for key in self._lazy_keys if key not in stored]
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:
# The dict's own test, since `__contains__` below claims every lazy key.
if key in self._lazy_keys and not super().__contains__(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:
return self[key]
return super().get(key, default)
def __iter__(self) -> Iterator[str]:
"""Iterate the stored keys, then the pending ones."""
yield from super().__iter__()
yield from self._pending()
def __reversed__(self) -> Iterator[str]:
yield from reversed(self._pending())
yield from super().__reversed__()
def __len__(self) -> int:
return super().__len__() + len(self._pending())
# Views over the mapping itself rather than over the stored entries, so they
# list the pending keys too.
def keys(self) -> KeysView[str]: # type: ignore[override]
return KeysView(self)
def values(self) -> ValuesView[Any]: # type: ignore[override]
return ValuesView(self)
def items(self) -> ItemsView[str, Any]: # type: ignore[override]
return ItemsView(self)
def __eq__(self, other: object) -> bool:
return dict(self.items()) == other
def __ne__(self, other: object) -> bool:
return not self == other
def __delitem__(self, key: str) -> None:
# A pending key has no stored entry: forgetting it is enough.
if self._lazy_keys.pop(key, None) is None or super().__contains__(key):
super().__delitem__(key)
def pop(self, key: str, *default: Any) -> Any:
if key in self._lazy_keys:
value = self[key]
del self[key]
return value
return super().pop(key, *default)
def popitem(self) -> tuple[str, Any]:
"""Remove and return the last pair, in the order of iteration."""
pending = self._pending()
if pending:
return pending[-1], self.pop(pending[-1])
key, value = super().popitem()
self._lazy_keys.pop(key, None)
return key, value
def setdefault(self, key: str, default: Any = None) -> Any:
if key in self._lazy_keys:
return self[key]
return super().setdefault(key, default)
def clear(self) -> None:
self._lazy_keys.clear()
super().clear()
def copy(self) -> _LazyMetaDict:
"""A shallow copy, whose pending keys stay pending."""
return _LazyMetaDict(self, self._source, tuple(self._lazy_keys.values()))
# --- 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.show_params.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 a live CLI needs: landing it retires the replay
behind the `--params` table and the configuration dump. The
`click-extra wrap --params` path keeps this constant and its replay, because
Click never parses the foreign CLI it renders.
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.make_context stashes a copy of the raw argv under
# RAW_ARGS; show_params.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. `parse_args` rewrites the UNSET
# sentinel of a parameter with no default to None and the re-parse does not,
# so the consumer sites (render_params_table and _format_param_row) normalize
# it back to None.
# - It depends on `Command.make_parser()` handing back the private
# `click.parser._OptionParser`.
#
# 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 RAW_ARGS does.
# 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.
#
# Reusing the `opts` lying around does not work: `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 {meth}`~click_extra.config.option.ConfigOption.load_conf` when a
`config_schema` is set. 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.jobs.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_options.TableFormatOption.init_formatter`.
Read by {meth}`~click_extra.commands.Context.render_table` and
{meth}`~click_extra.commands.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_options.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_options.SortByOption.init_sort` when its
column definitions are known at declaration time (labeled mode). Read by
{meth}`~click_extra.commands.Context.render_table` and
{meth}`~click_extra.commands.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_options.ColumnsOption.init_columns`. Read by
table-rendering consumers (like {class}`click_extra.show_params.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.trail.OperationTrail` and
{func}`~click_extra.progress.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.
"""
# --- Internal keys ------------------------------------------------------------
# Not part of the public API: one module writes and reads each of them. They live
# here, under the `click_extra._` prefix, so that this module spells every key
# Click Extra puts in `ctx.meta`.
_COLOR_CLI_OVERRIDE: Final[str] = "click_extra._color_cli_override"
"""Set when `--no-color` is passed on the command line.
Written by {meth}`click_extra.color.NoColorOption.set_no_color` and read by
{meth}`click_extra.color.ColorOption.set_color`, so the explicit negative alias
outranks the color environment variables, which may only override the built-in
default.
"""
_COLOR_PUBLISHED: Final[str] = "click_extra._color_published"
"""Set once the reset of the process-wide color mirror is queued.
Written by {func}`click_extra.color.publish_invocation_color`, which so registers
its reset callbacks at most once per invocation, however many color callbacks
fire.
"""
_CONFIG_PRELOADED: Final[str] = "click_extra._config_preloaded"
"""The command paths whose configuration was loaded ahead of time.
Filled by `ConfigOption._resolve_subcommands_eagerly`, which processes the option
before Click's parameter loop does. That loop then calls
{meth}`click_extra.config.option.ConfigOption.load_conf` again with the same value:
the call removes its entry and returns, in place of loading the document a second
time.
"""
_CONFIG_STATUS_SILENCED: Final[str] = "click_extra._config_status_silenced"
"""Whether the command line asks for less than `WARNING` output.
Set by {meth}`click_extra.config.option.ConfigOption.handle_parse_result` before
the configuration loads, and read by
{meth}`click_extra.config.option.ConfigOption.load_conf` to keep its status lines
quiet.
"""
_LOG_HANDLER_TOKEN: Final[str] = "click_extra._log_handler_token"
"""The token a run holds the log handler with.
`ctx.meta` is shared down the context hierarchy, so the options of a command and
of its subcommands find the same token there, and a run takes one hold however
many of them it carries. See `click_extra.logging._hold_invocation_handler`.
"""
_LOG_LEVEL_TOKEN: Final[str] = "click_extra._log_level_token"
"""The token a run owns its log levels with.
Set by the first verbosity option that applies a level, which also queues the
reset of the loggers. It lives in `ctx.meta` so the verbosity options queue that
reset at most once per invocation, even when several of them are passed. See
`click_extra.logging._level_owner`.
"""
_VERBOSITY_COUNTER_SCANNED: Final[str] = "click_extra._verbosity_counter_scanned"
"""Set once the `-v`/`-q` counter is resolved ahead of the verbosity options.
The first verbosity option reaching `_VerbosityOption.handle_parse_result` reads
the raw `--verbose`/`--quiet` repetition counts from the parsed `opts` and
stashes them on the context before any option applies a level. This way the
option that fires first already reconciles the full counter and never applies an
intermediate, louder level that a later `-q` would have to lower, which would
leak its `Set ... DEBUG` debug trace.
"""
# --- Helpers ------------------------------------------------------------------
[docs]
def pass_context(func: Callable[Concatenate[Context, P], R]) -> Callable[P, R]:
"""Mark a callback as wanting the active {class}`~click_extra.commands.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}`~click_extra.commands.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}`~click_extra.commands.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`.
"""
@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
[docs]
def setdefault(ctx: click.Context, key: str, factory: Callable[[], Any]) -> Any:
"""Read `key` from the shared `meta` dict, first storing `factory()` if missing.
`factory` runs for a missing key only, so each invocation gets a mutable value
of its own, like the set of command paths a run marks.
"""
if key not in ctx.meta:
ctx.meta[key] = factory()
return ctx.meta[key]