Source code for click_extra.parameters

# 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.
"""Our own flavor of `Option`, `Argument` and `parameters`."""

from __future__ import annotations

import logging
from collections import ChainMap
from contextlib import nullcontext
from functools import cached_property, reduce
from gettext import gettext as _
from operator import getitem
from typing import TypeVar

import click
import cloup
from boltons.pathutils import shrinkuser
from click import ParamType, get_current_context
from click._utils import UNSET
from click.core import ParameterSource
from deepmerge import always_merger

from . import context

# Imported under a private name so this module's namespace does not resurrect
# the moved helper: its canonical home is click_extra._utils, and the public
# `parameters.patch_attr` spelling resolves through the deprecation hook below.
from ._utils import patch_attr as _patch_attr
from .envvar import param_envvar_ids
from .styling import Style
from .types import EnumChoice

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator, Sequence
    from pathlib import Path
    from typing import Any, ClassVar, Literal

    from boltons.urlutils import URL

logger = logging.getLogger(__name__)

P = TypeVar("P", bound=click.Parameter)
"""Type variable bound to {class}`click.Parameter`, letting
{func}`require_sibling_param` return the exact subclass it was asked to find."""

#: Separator joining the keys of a parameter's fully-qualified path
#: (`cli.subcommand.param`).
PARAM_PATH_SEP = "."


[docs] def search_params( params: Iterable[click.Parameter], klass: type[click.Parameter], include_subclasses: bool = True, unique: bool = True, ) -> list[click.Parameter] | click.Parameter | None: """Search a particular class of parameter in a list and return them. :param params: list of parameter instances to search in. :param klass: the class of the parameters to look for. :param include_subclasses: if `True`, includes in the results all parameters subclassing the provided `klass`. If `False`, only matches parameters which are strictly instances of `klass`. Defaults to `True`. :param unique: if `True`, raise an error if more than one parameter of the provided `klass` is found. Defaults to `True`. """ param_list = [ p for p in params if (include_subclasses and isinstance(p, klass)) or (not include_subclasses and p.__class__ is klass) ] if not param_list: return None if unique: if len(param_list) != 1: raise RuntimeError( f"More than one {klass.__name__} parameters found on command: " f"{param_list}" ) return param_list.pop() return param_list
[docs] def last_param( params: Iterable[click.Parameter], klass: type[click.Parameter], ) -> click.Parameter | None: """Return the last parameter of exactly `klass` in *params*, or `None`. Unlike {func}`search_params`, this matches the exact `klass` (no subclasses) and tolerates duplicates: when an option is declared more than once (like an explicit `@verbosity_option` stacked on a Click Extra command that already ships one), Click keeps the last occurrence, so this mirrors that here instead of erroring out on the ambiguity. :param params: the command's parameter list to scan. :param klass: the exact parameter class to look for. """ options = search_params(params, klass, include_subclasses=False, unique=False) return options[-1] if options else None # type: ignore[index]
[docs] def require_sibling_param( params: Iterable[click.Parameter], requester: click.Parameter, klass: type[P], ) -> P: """Return the sibling *klass* parameter declared on the same command, or raise. Some options are inert on their own: they drive machinery owned by a sibling option. `--no-config` and `--validate-config`, for instance, both depend on the `--config` option ({class}`~click_extra.config.option.ConfigOption`). This helper centralizes the lookup so every such option raises the same `RuntimeError` when its required sibling is missing, naming the offending flag. :param params: the command's parameter list to scan. :param requester: the parameter requiring the sibling, used to build the error message from its flag names and class. :param klass: the sibling parameter class to look for. """ sibling = search_params(params, klass) if not isinstance(sibling, klass): # RuntimeError (not the type-implied TypeError) is intentional: it keeps # the historical --no-config contract and unifies all call sites on one # exception type for a missing-or-wrong-type sibling. raise RuntimeError( # noqa: TRY004 f"{'/'.join(requester.opts)} {type(requester).__name__} must be used " f"alongside {klass.__name__}." ) return sibling
[docs] def full_short_help(command: click.Command) -> str: """Return the command's canonical one-line short help, untruncated. Click's {meth}`click.Command.get_short_help_str` truncates to 45 characters by default with a trailing `"..."` so subcommand listings fit a terminal column. That bound is wrong for generated documentation and completion specs, where the NAME / COMMANDS sections carry the full description and the renderer wraps text on its own. The lookup mirrors Click's order: an explicit `short_help` wins, otherwise the first paragraph of `command.help` is joined into one line. A truthy `deprecated` flag prepends `(Deprecated)` so the flag stays visible. """ if command.short_help: text = command.short_help.strip() elif command.help: # Click already stores `help` after `inspect.cleandoc`: split on the # first blank line to grab the leading paragraph, then squash internal # newlines so the result is one line. paragraph = command.help.split("\n\n", 1)[0] text = paragraph.strip().replace("\n", " ") else: text = "" if command.deprecated: text = f"(Deprecated) {text}".strip() return text
[docs] def resolve_param_help(param: click.Parameter, ctx: click.Context) -> str | None: """Return a parameter's help text, including the dynamically-generated ones. Reading `param.help` covers the options that carry a static string, and misses the ones that compute their help from the context: Click Extra's own `-v` / `-q` derive theirs from the resolved base verbosity, and leave the attribute at `None` (see {meth}`~click_extra.logging.VerboseOption.get_help_record`). Falling back to the help record picks those up. The record also carries Click's bracket fields (`[default: …]`, `[required]`, `[env var: …]`), appended to the prose behind two spaces. They are stripped here: they are not the author's documentation, and every backend of this module renders them (or deliberately does not) from structured fields of its own. """ help_text = getattr(param, "help", None) if help_text: return str(help_text) record = param.get_help_record(ctx) if not record: return None text = record[1] if isinstance(param, click.Option) and param.get_help_extra(ctx): marker = text.rfind(" [") if marker != -1: text = text[:marker] return text.strip() or None
[docs] def param_spellings(param: click.Parameter) -> tuple[str, ...]: """All literal spellings of a parameter: primary `opts` then `secondary_opts`. A boolean flag pair yields both forms (`--foo`, `--no-foo`); a plain option yields just its declared names. """ return tuple(param.opts) + tuple(param.secondary_opts)
[docs] def short_long_opts(opts: Sequence[str]) -> tuple[str, str]: """Split option spellings into the first short (`-x`) and long (`--xy`) form. Either element is the empty string when that form is absent. """ short = next((o for o in opts if o.startswith("-") and not o.startswith("--")), "") long = next((o for o in opts if o.startswith("--")), "") return short, long
[docs] def option_value_kind( param: click.Parameter, ) -> Literal["flag", "optional", "required"]: """Classify how an option consumes a value, the basis for rendering its metavar. - `"flag"`: takes no value. A boolean switch (`--foo`, `--foo/--no-foo`), a flag with a custom `flag_value` (`--no-config`), or a counter (`-v`). - `"optional"`: the value may be omitted. Click models this as `is_flag=False` with a `flag_value` set, so a bare `--color` stands for the flag value while `--color=never` passes an explicit one. - `"required"`: consumes a value (`--config CONFIG_PATH`). ```{note} The discriminator is Click's `is_flag` (plus `count`), not `is_bool_flag`: a flag carrying a custom `flag_value` such as {class}`~click_extra.config.option.NoConfigOption` reports `is_bool_flag=False` yet still takes no value. ``` """ if getattr(param, "count", False) or getattr(param, "is_flag", False): return "flag" if getattr(param, "secondary_opts", None): return "flag" flag_value = getattr(param, "flag_value", None) # Click's development branch models an absent flag_value as the UNSET # sentinel, not None. if flag_value is not None and flag_value is not UNSET: return "optional" return "required"
[docs] def resolve_flag_value(param: click.Parameter) -> Any: """The value *param*'s primary declaration stands for. Released Click materializes it in `flag_value`: `None` for a plain option or a counter, `True` for a boolean flag, and the declared value otherwise (`--color` standing for `always`). Click's development branch leaves that attribute as the `UNSET` sentinel and answers the same question lazily in `flag_activation_value`, so reading either attribute on its own is right on only one of the two, and storing the sentinel anywhere it will be read back as a value silently turns the flag off. """ flag_value = getattr(param, "flag_value", None) if flag_value is UNSET: return getattr(param, "flag_activation_value", None) return flag_value
[docs] def is_repeatable(param: click.Parameter) -> bool: """Whether the parameter may be supplied several times (`multiple` or `count`).""" return bool(getattr(param, "multiple", False) or getattr(param, "count", False))
class _ParameterMixin: """Mixin providing shared functionality for Click Extra parameters. ```{warning} If we want to override any method from Click's `Parameter` class, we have to use that mixin and have it inherited first in the `Option` and `Argument` classes below. Because: - Cloup does not provide its own `Parameter` class. - Multiple inheritance cannot be used because of MRO issues. ``` """ # Attributes provided by the `click.Parameter` subclass this mixin is always # combined with. Declared here so the methods below can reference them without # mypy flagging them as undefined on the standalone mixin. multiple: bool nargs: int def get_default(self, ctx: click.Context, call: bool = True): """Override `click.Parameter.get_default()` to support `EnumChoice` types. Reuse the `EnumChoice.get_choice_string()` method to convert an `Enum` default value to its string representation, to bypass [Click's default behavior of returning the Enum.name](https://github.com/pallets/click/pull/3004). ```{note} A `multiple` option or a variadic (`nargs=-1`) parameter carries a *sequence* of members as its default, so each member is resolved on its own. Converting the sequence as a whole would stringify the tuple itself (`str((MyEnum.FOO,))`) and later trip Click's ``Value must be an iterable`` check on the default-value path. ``` """ default_value = super().get_default(ctx, call) # type: ignore[misc] if ( hasattr(self, "type") and isinstance(self.type, EnumChoice) # Turns out UNSET is also an Enum member, so we need to ignore it. and default_value is not UNSET ): if self.multiple or self.nargs == -1: # A `None` default is not iterable; leave it for Click to turn # into an empty tuple. if default_value is not None: default_value = tuple( self.type.get_choice_string(member) for member in default_value ) else: default_value = self.type.get_choice_string(default_value) return default_value
[docs] class Argument(_ParameterMixin, cloup.Argument): """Wrap `cloup.Argument`, itself inheriting from `click.Argument`. Inherits first from `_ParameterMixin` to allow future overrides of Click's `Parameter` methods. """
[docs] class Option(_ParameterMixin, cloup.Option): """Wrap `cloup.Option`, itself inheriting from `click.Option`. Inherits first from `_ParameterMixin` to allow future overrides of Click's `Parameter` methods. """
[docs] class ExtraOption(Option): """Dedicated to option implemented by `click-extra` itself. Provides a way to identify Click Extra's own options with certainty, and restores the pre-Click-8.4.0 contract that a callback (or a type's `convert()`) can introspect its own parameter source from within itself. ```{note} This is the one click-extra class that deliberately keeps the `Extra` prefix. The `8.0.0` cleanup dropped it everywhere else (`ExtraCommand` became `Command`, `ExtraContext` became `Context`, and so on), shadowing the matching Cloup or Click class. Here the plain `Option` name is already taken by the user-facing enhanced wrapper this class subclasses, so the prefix is not legacy baggage but a real distinction: `ExtraOption` marks click-extra's *own* built-in options. That marker is load-bearing, since {class}`~click_extra.commands.Command` sorts parameters with `isinstance(param, ExtraOption)` to push the built-in options to the end. ``` ```{note} Bracket fields (envvar, default, range, required) cannot be pre-styled in `get_help_record()` because Click's text wrapper splits lines *after* the record is returned, which would break ANSI codes that span wrapped boundaries. Styling is instead applied post-wrapping in `HelpFormatter._style_bracket_fields()`, which uses the structured data from `Option.get_help_extra()` to identify each field by its label. ``` ```{note} Built-in option subclasses share a common shape: their `__init__` defaults `param_decls` to the option's canonical flags and wires an eager callback via `kwargs.setdefault("callback", self.<callback>)`. Every callback name encodes its role with a verb prefix. The common roles are: - `set_<key>` publishes a resolved value to `ctx.meta` (`set_color`, `set_no_color`, `set_theme`, `set_telemetry`, `set_progress`, `set_accessible`, `set_zero_exit`, the verbosity options' `set_level`); - `init_<system>` additionally installs a `ctx` helper or records a snapshot (`init_timer`, `init_formatter`, `init_columns`, `init_sort`); - `validate_<thing>` coerces and validates the raw input (`validate_jobs`, `validate_config`); - `print_*` renders output and exits (`print_man`, `print_params`, `print_and_exit`). A few options own a richer operation and name it with its own verb rather than forcing one of the above. `ConfigOption` wires `load_conf` to read, parse, and merge a configuration file, and `NoConfigOption` wires `check_sibling_config_option` to assert that a sibling `--config` option exists. ``` """
[docs] def handle_parse_result(self, ctx, opts, args): """Record the parameter source before delegating to the base implementation. ```{warning} Click `8.4.0` (PR [pallets/click#3404](https://github.com/pallets/click/pull/3404)) reordered `Parameter.handle_parse_result` so `ctx.set_parameter_source` runs *after* `process_value`. Callbacks that introspect their own provenance via `ctx.get_parameter_source(self.name)` therefore read `None` instead of the actual source. `ColorOption`, `ConfigOption`, and `ShowParamsOption` rely on this introspection (from their eager callback) to decide whether an env var should override the default (`--color`), whether the `--config` path was user-supplied, and what to render in the `Source` column of `--params`. `JobsOption` relies on the same introspection from its type's non-eager `convert()` ({class}`~click_extra.execution.JobCount`), to decide whether an `auto`/`max` collapsing to a single job logs as a warning (explicit request) or at info level (the option's own default). Click `8.4.1` restored the pre-`8.4.0` contract upstream (PR [pallets/click#3484](https://github.com/pallets/click/pull/3484)), so this override only matters for Click `8.4.0` itself, which sits inside click-extra's supported `>= 8.3.1` range. Pre-recording the source here, for every option regardless of eagerness, keeps that contract on every supported Click. `super().handle_parse_result` re-records the same value at the canonical time, so the slot arbitration logic introduced by #3404 is unaffected: `slot_empty` is computed from `ctx.params`, not from `_parameter_source`. `consume_value` runs twice as a side effect: once here and once in `super`. Both calls are pure for click-extra's existing options (no env var side effects, no prompt): `consume_value` only resolves the raw value and its source, it never invokes the parameter's `type.convert()`, so this pre-record cannot itself trigger a callback's or a type's logging or validation twice. Should a future subclass need prompt behavior, this override would need to cache the result instead. The pre-record is skipped when the slot already carries a source from an earlier option sharing the same `name` (Click's feature-switch pattern), so the arbitration logic in `super` still sees the original `existing_source` rather than a stale rewrite from this option. ``` """ if ctx.get_parameter_source(self.name) is None: _value, source = self.consume_value(ctx, opts) ctx.set_parameter_source(self.name, source) return super().handle_parse_result(ctx, opts, args)
[docs] class ParamStructure: """Utilities to introspect CLI options and commands structure. Structures are represented by a tree-like `dict`. Access to a node is available using a serialized path string composed of the keys to descend to that node, separated by a dot `.`. """ excluded_params: frozenset[str] """Fully-qualified IDs of the parameters to block from the structure. Set by subclasses: {class}`ShowParamsOption` freezes an empty set, while `ConfigOption` resolves a dynamic default (or the user-provided list) within the active context. The two filters are mutually exclusive, a constraint each subclass enforces in its own constructor. """ included_params: frozenset[str] | None """Allowlist of parameter IDs, mutually exclusive with `excluded_params`. `None` disables the allowlist. It is resolved into `excluded_params` by {meth}`~click_extra.parameters.ParamStructure.build_param_trees`, once every parameter ID is known. """
[docs] @staticmethod def init_tree_dict(*path: str, leaf: Any = None) -> Any: """Utility method to recursively create a nested dict structure whose keys are provided by `path` list and at the end is populated by a copy of `leaf`.""" def dive(levels): if levels: return {levels[0]: dive(levels[1:])} return leaf return dive(path)
[docs] @staticmethod def get_tree_value(tree_dict: dict[str, Any], *path: str) -> Any: """Get in the `tree_dict` the value located at the `path`. Raises `KeyError` if no item is found at the provided `path`. """ return reduce(getitem, path, tree_dict)
[docs] def walk_params(self) -> Iterator[tuple[tuple[str, ...], click.Parameter]]: """Generate an unfiltered list of all CLI parameters. Everything is included, from top-level groups to subcommands, and from options to arguments. Yields a 2-element tuple: - a tuple of keys leading to the parameter; - the parameter object itself. Thin adapter over {func}`~click_extra.parameters.walk_command_params`: it resolves the root CLI from the active context and drops the per-parameter context that the free function also yields. """ ctx = get_current_context() cli = ctx.find_root().command assert cli.name is not None for keys, param, _ctx in walk_command_params(cli, ctx, (cli.name,)): yield keys, param
TYPE_MAP: ClassVar[dict[type[ParamType], type[str | int | float | bool | list]]] = { click.types.StringParamType: str, click.types.IntParamType: int, click.types.FloatParamType: float, click.types.BoolParamType: bool, click.types.UUIDParameterType: str, click.types.UnprocessedParamType: str, click.types.File: str, click.types.Path: str, click.types.Choice: str, click.types.IntRange: int, click.types.FloatRange: float, click.types.DateTime: str, click.types.Tuple: list, } """Map Click types to their Python equivalent. Keys are subclasses of `click.types.ParamType`. Values are expected to be simple builtins Python types. This mapping can be seen as a reverse of the `click.types.convert_type()` method. """
[docs] @staticmethod def map_click_type(click_type: ParamType) -> type[str | int | float | bool | list]: """Map a Click parameter type instance to its Python equivalent. Returns `str` for unrecognised custom types, since command-line parameters are strings by default. See the list of [custom types provided by Click](https://click.palletsprojects.com/en/stable/api/#types). """ # Try to directly map the Click type to a Python type. py_type = ParamStructure.TYPE_MAP.get(click_type.__class__) if py_type is not None: return py_type # Try to indirectly map the type by looking at inheritance. for param_type, mapped_type in ParamStructure.TYPE_MAP.items(): if isinstance(click_type, param_type): return mapped_type # Custom parameters are expected to convert from strings, as that's # the default type of command lines. # See: https://click.palletsprojects.com/en/stable/api/#click.ParamType return str
[docs] @staticmethod def get_param_type( param: click.Parameter, ) -> type[str | int | float | bool | list]: """Get the Python type of a Click parameter. Returns `str` for unrecognised custom types, since command-line parameters are strings by default. See the list of [custom types provided by Click](https://click.palletsprojects.com/en/stable/api/#types). """ if param.multiple or param.nargs != 1: return list if hasattr(param, "is_bool_flag") and param.is_bool_flag: return bool return ParamStructure.map_click_type(param.type)
[docs] def build_param_trees(self) -> dict[str, Any]: """Build and return the parameters tree structure. This removes parameters whose fully-qualified IDs are in the `excluded_params` blocklist. If `included_params` was provided, it is resolved into `excluded_params` here, where all parameter IDs are available. """ # Resolve included_params into excluded_params before filtering. if self.included_params is not None: all_param_ids = frozenset( PARAM_PATH_SEP.join(keys) for keys, _ in self.walk_params() ) self.excluded_params = all_param_ids - self.included_params objects: dict[str, Any] = {} for keys, param in self.walk_params(): if PARAM_PATH_SEP.join(keys) in self.excluded_params: continue objects = always_merger.merge( objects, self.init_tree_dict(*keys, leaf=[param]) ) return objects
@staticmethod def _nullify_leaves(tree: dict[str, Any]) -> dict[str, Any]: """Derive a template shape from a tree by replacing all leaves with `None`.""" return { k: ParamStructure._nullify_leaves(v) if isinstance(v, dict) else None for k, v in tree.items() }
[docs] @cached_property def params_template(self) -> dict[str, Any]: """Returns a tree-like dictionary whose keys shadows the CLI options and subcommands and values are `None`. Perfect to serve as a template for configuration files. """ return self._nullify_leaves(self.params_objects)
[docs] @cached_property def params_objects(self) -> dict[str, Any]: """Returns a tree-like dictionary whose keys shadows the CLI options and subcommands and values are parameter objects. Perfect to parse configuration files and user-provided parameters. """ return self.build_param_trees()
[docs] def get_param_spec(param: click.Parameter, ctx: click.Context) -> str | None: """Extract the option-spec string (like `-v, --verbose`) from a parameter. Temporarily unhides hidden options so their help record can be produced. ```{note} The `hidden` property is only supported by `Option`, not `Argument`. ``` ```{todo} Submit a PR to Click to separate production of param spec and help record. That way we can always produce the param spec even if the parameter is hidden. See: https://github.com/kdeldycke/click-extra/issues/689 ``` """ if not hasattr(param, "hidden"): return None with _patch_attr(param, "hidden", False) if param.hidden else nullcontext(): help_record = param.get_help_record(ctx) return help_record[0] if help_record else None
def _structured_value(value: Any) -> Any: """Coerce a value to a natively serializable form for structured output. Scalars and lists (`str`, `int`, `float`, `bool`, `list`, `None`) pass through unchanged so JSON, YAML and TOML renderers keep their native types. Anything else (a Click `ParamType`, a `Path`, an arbitrary object) is rendered with {func}`repr` so it survives serialization as a readable string. """ if isinstance(value, (str, int, float, bool, list, type(None))): return value return repr(value)
[docs] def format_param_row( param: click.Parameter, ctx: click.Context, path: str, is_structured: bool, ) -> dict[str, Any]: """Compute the *structural* table cells for a Click parameter. Returns a `dict[column_id, cell]` covering every column that can be derived from the parameter object alone (no runtime invocation state or config-file context). Specifically: `id`, `spec`, `class`, `param_type`, `python_type`, `hidden`, `exposed`, `envvars`, `default`, `is_flag`, `flag_value`, `is_bool_flag`, `multiple`, `nargs`, `prompt`, and `confirmation_prompt`. Attributes only defined on `click.Option` (`hidden`, `is_flag`, `flag_value`, `is_bool_flag`, `prompt`, `confirmation_prompt`) yield `None` for `click.Argument` parameters: empty cell in visual formats, `null` in structured ones. For structured formats (JSON, YAML, etc.), values are native Python types. For visual formats, values are themed strings matching help-screen styling. The remaining table columns (`allowed_in_conf`, `value`, `source`, `config_file`) require live context and are filled in by {func}`~click_extra.parameters.render_params_table`. """ param_spec = get_param_spec(param, ctx) param_class = param.__class__ class_str = f"{param_class.__module__}.{param_class.__qualname__}" type_str = f"{param.type.__module__}.{param.type.__class__.__name__}" python_type_name = ParamStructure.get_param_type(param).__name__ hidden = getattr(param, "hidden", None) is_flag = getattr(param, "is_flag", None) flag_value = resolve_flag_value(param) is_bool_flag = getattr(param, "is_bool_flag", None) prompt = getattr(param, "prompt", None) confirmation_prompt = getattr(param, "confirmation_prompt", None) # Click 8.4 returns the UNSET sentinel (not None) for parameters that have no # default. Present it as None, mirroring the normalization # click.Command.parse_args applies to ctx.params. See the RAW_ARGS dossier in # click_extra.context for the full rationale. default_val = param.get_default(ctx) if default_val is UNSET: default_val = None if is_structured: default_val = _structured_value(default_val) flag_value = _structured_value(flag_value) return { "id": path, "spec": param_spec, "help": resolve_param_help(param, ctx), "class": class_str, "param_type": type_str, "python_type": python_type_name, "hidden": hidden, "exposed": param.expose_value, "envvars": list(param_envvar_ids(param, ctx)), "default": default_val, "is_flag": is_flag, "flag_value": flag_value, "is_bool_flag": is_bool_flag, "multiple": param.multiple, "nargs": param.nargs, "prompt": prompt, "confirmation_prompt": confirmation_prompt, } # Lazy import to avoid circular dependency with theme. from .theme import KO_GLYPH, OK_GLYPH, get_current_theme active_theme = get_current_theme() def styled_bool(value): """Render a boolean attribute as a themed glyph, or `None` if absent.""" if value is None: return None return ( active_theme.success(OK_GLYPH) if value is True else active_theme.error(KO_GLYPH) ) return { "id": active_theme.invoked_command(path), "spec": active_theme.option(param_spec) if param_spec else param_spec, "help": resolve_param_help(param, ctx), "class": class_str, "param_type": type_str, "python_type": active_theme.metavar(python_type_name), "hidden": styled_bool(hidden), "exposed": styled_bool(param.expose_value), "envvars": ", ".join(map(active_theme.envvar, param_envvar_ids(param, ctx))), "default": active_theme.default(repr(default_val)), "is_flag": styled_bool(is_flag), "flag_value": ( active_theme.default(repr(flag_value)) if flag_value is not None else None ), "is_bool_flag": styled_bool(is_bool_flag), "multiple": styled_bool(param.multiple), "nargs": str(param.nargs), "prompt": prompt, "confirmation_prompt": styled_bool(confirmation_prompt), }
[docs] def make_resilient_context( command: click.Command, info_name: str | None = None, parent: click.Context | None = None, ) -> click.Context: """Build an introspection context for a command. Parses no arguments and sets `resilient_parsing=True` so required-argument errors, prompts and eager-option side effects stay dormant: the canonical way to materialize a {class}`click.Context` purely to read a command's structure (its parameters, env-var prefix and subcommands), shared by the man-page and Carapace exporters. """ return command.make_context(info_name, [], parent=parent, resilient_parsing=True)
[docs] def iter_subcommands( command: click.Command, ctx: click.Context, *, skip_hidden: bool = True, ) -> Iterator[tuple[str, click.Command]]: """Yield a group's direct subcommands as `(name, command)` pairs. Subcommands are discovered dynamically through {meth}`click.Group.list_commands` / {meth}`~click.Group.get_command`, in listing order, so lazily-registered commands are included. A non-group yields nothing, a name resolving to `None` is skipped, and hidden subcommands are skipped unless `skip_hidden` is `False` (completion specs keep them, flagged hidden; documentation drops them). """ if not isinstance(command, click.Group): return for name in command.list_commands(ctx): sub = command.get_command(ctx, name) if sub is None: continue if skip_hidden and getattr(sub, "hidden", False): continue yield name, sub
[docs] def iter_params_for_display( command: click.Command, ctx: click.Context, ) -> Iterator[click.Parameter]: """Yield a command's parameters in the order its help screen lists them. A Click Extra command keeps two orders apart: `command.params` is the processing order, which decides when each callback fires, while the help screen reads the presentation order Cloup caches in `arguments`, `option_groups` and `ungrouped_options` (see {meth}`click_extra.commands.Command.param_priority`). Reading `get_params()` therefore renders a man page, a Markdown document or a completion spec whose flags no longer match the `--help` its reader just saw. This is the accessor every such renderer should go through. Falls back to `get_params()` for a command that carries no Cloup option groups, where the two orders are the same list. Any parameter attached after construction, and so absent from the cached groups, is yielded last rather than dropped. """ if not isinstance(command, cloup.Command): yield from command.get_params(ctx) return seen: set[int] = set() ordered: list[click.Parameter] = [ *command.arguments, *(option for group in command.option_groups for option in group.options), *command.get_ungrouped_options(ctx), ] for param in ordered: if id(param) not in seen: seen.add(id(param)) yield param # A parameter appended to `command.params` after construction never made it into # the cached groups. Yield it rather than silently dropping it from the render. for param in command.get_params(ctx): if id(param) not in seen: seen.add(id(param)) yield param
[docs] def walk_command_params( cmd: click.Command, ctx: click.Context, parent_keys: tuple[str, ...] = (), ) -> Iterator[tuple[tuple[str, ...], click.Parameter, click.Context]]: """Walk the parameter tree of a Click command and all its subcommands. Yields `(path_keys, param, owning_ctx)` for every parameter found on *cmd* and, recursively, on each subcommand. Each subcommand is walked under its own freshly-built child context, so context-sensitive metadata (notably the auto-generated environment variable, which derives from `Context.auto_envvar_prefix`) is computed at the correct nesting level rather than inherited from the root. A subcommand whose name collides with a sibling parameter at the same level is skipped: a single fully-qualified path cannot address both an option and a subcommand at once. """ level_param_names = set() for param in cmd.get_params(ctx): if param.name is not None: level_param_names.add(param.name) yield (*parent_keys, param.name), param, ctx if isinstance(cmd, click.Group): # `list_commands()` is the group's own authority on subcommand order: a # stock Click group still answers alphabetically, and one that was told # otherwise gets a `--params` tree matching its help screen. for subcmd_name in cmd.list_commands(ctx): if subcmd_name in level_param_names: logger.debug( f"{cmd.name}{PARAM_PATH_SEP}{subcmd_name} subcommand shadows a " f"top-level parameter; excluded from parameter tree.", ) continue subcmd = cmd.get_command(ctx, subcmd_name) if subcmd is None: continue subcmd_ctx = click.Context(subcmd, parent=ctx, info_name=subcmd_name) yield from walk_command_params( subcmd, subcmd_ctx, (*parent_keys, subcmd_name) )
[docs] def replay_raw_args(subject_ctx: click.Context) -> dict[str, Any]: """Re-parse the captured raw arguments to recover per-parameter values. Click discards the pre-parsed arguments once processing is done, so the value and provenance of each parameter cannot be read back directly. When {data}`~click_extra.context.RAW_ARGS` was captured on the context (by `Command`/`Group`), replaying it through a fresh parser rebuilds the `opts` mapping that `Parameter.consume_value` consults, without re-firing eager callbacks: `handle_parse_result` is never called here, only the parser. Returns an empty mapping when no raw arguments were captured, so callers can fall back to parameter defaults. """ raw_args = context.get(subject_ctx, context.RAW_ARGS) if raw_args is None: return {} parser = subject_ctx.command.make_parser(subject_ctx) opts, _, _ = parser.parse_args(args=list(raw_args)) return opts
[docs] def param_config_source( root_ctx: click.Context, keys: Sequence[str], ) -> Path | URL | None: """Return the configuration file supplying a parameter's `default_map` value. *keys* is the parameter's fully-qualified path, root command name first and parameter name last, as yielded by {func}`walk_command_params`. Returns `None` when no configuration file was loaded, when the parameter is absent from every loaded layer, or when the context carries no layered `default_map` at all. The walk mirrors how Click resolves `default_map`, so the attribution matches the value Click actually picks: - Root-level parameters are looked up in the root context's `~collections.ChainMap`, whose first layers are the loaded files in precedence order (see {data}`~click_extra.context.CONF_SOURCES`): the first layer naming the parameter wins. - A subcommand section resolves against that same `ChainMap`, but Click keeps descending *inside the single layer* that named the first segment: the file owning a subcommand section owns its whole sub-tree. ```{note} Lazy subcommands receive their section from the merged configuration document, injected into the front layer by {meth}`~click_extra.commands.Group._apply_config_to_parent_context`: those values are attributed to the highest-precedence file even when several files contributed to the merge. ``` """ sources: tuple[tuple[Path | URL, dict[str, Any]], ...] | None = context.get( root_ctx, context.CONF_SOURCES ) if not sources: return None default_map = root_ctx.default_map if not isinstance(default_map, ChainMap): return None layers = default_map.maps[: len(sources)] segments = keys[1:-1] param_name = keys[-1] if segments: for index, layer in enumerate(layers): if segments[0] not in layer: continue subtree = layer[segments[0]] for segment in segments[1:]: if not isinstance(subtree, dict): return None subtree = subtree.get(segment) if isinstance(subtree, dict) and param_name in subtree: return sources[index][0] return None return None for index, layer in enumerate(layers): if param_name in layer: return sources[index][0] return None
[docs] def render_params_table( subject_ctx: click.Context, *, default_columns: Sequence[str] | None = None, ) -> None: """Introspect `subject_ctx.command` and print its parameter metadata table. Walks the command and any nested subcommands, emitting one row per parameter. The table format and column selection are read from `subject_ctx.meta` (see {data}`~click_extra.context.TABLE_FORMAT` and {data}`~click_extra.context.COLUMNS`); when neither is set, a sibling `--table-format` / `--columns` option on the command is consulted, then the *default_columns* fallback, then the canonical order. When `subject_ctx.meta` carries pre-parsed {data}`~click_extra.context.RAW_ARGS`, the `value` and `source` columns are resolved by replaying those arguments against the command parser; otherwise they fall back to the parameter defaults. This is the shared rendering core behind both {meth}`~click_extra.parameters.ShowParamsOption.print_params` (introspecting the live CLI) and the `click-extra wrap --params` path (introspecting a foreign target). The caller is responsible for exiting the context afterwards. ```{important} Click does not keep the raw, pre-parsed arguments around, so values and their provenance cannot be read back directly. The workaround replays {data}`~click_extra.context.RAW_ARGS` (captured on the context by `Command`/`Group`) through the command parser, calling `consume_value()` rather than `handle_parse_result()` so eager callbacks are not re-triggered. ``` """ # Imported here to avoid circular imports with the config, table and theme # modules, which all import from this one. from .config import ConfigOption from .table import ( DEFAULT_FORMAT, SERIALIZATION_FORMATS, ColumnsOption, TableFormatOption, print_table, select_columns, select_row, ) from .theme import KO_GLYPH, OK_GLYPH, get_current_theme active_theme = get_current_theme() ok_styled = active_theme.success(OK_GLYPH) ko_styled = active_theme.error(KO_GLYPH) cmd = subject_ctx.command # Resolve the value getter. When the original arguments are available we # replay them through the command parser to recover each value and its # provenance; otherwise we only know the parameter defaults. Values are # resolved against each parameter's owning context, so subcommand # parameters read their `default_map` from the subcommand's own section # and their environment variables from the correct nesting level. opts: dict = {} raw_args = context.get(subject_ctx, context.RAW_ARGS) if raw_args is not None: logger.debug(f"{context.RAW_ARGS}: {raw_args}") opts = replay_raw_args(subject_ctx) def get_param_value(param, param_ctx): # consume_value() can return the UNSET sentinel for a parameter with # no user input and no default. Normalize it to None, mirroring the # step click.Command.parse_args runs after parsing, which this # re-parse bypasses. See the RAW_ARGS dossier in click_extra.context. value, source = param.consume_value(param_ctx, opts) return (None if value is UNSET else value), source else: def get_param_value(param, param_ctx): return None, param_ctx.get_parameter_source(param.name) # Locate a --config option to fill the "allowed in conf?" column. config_option = search_params(cmd.get_params(subject_ctx), ConfigOption) assert config_option is None or isinstance(config_option, ConfigOption) # Resolve the table format: an explicit context entry wins, else a sibling # --table-format option, else the default. if context.get(subject_ctx, context.TABLE_FORMAT) is None: table_option = search_params(cmd.get_params(subject_ctx), TableFormatOption) if table_option and isinstance(table_option, TableFormatOption): table_fmt, _ = table_option.consume_value(subject_ctx, opts) table_option.init_formatter( subject_ctx, table_option, table_option.type.convert(table_fmt, table_option, subject_ctx) if table_fmt else table_option.get_default(subject_ctx), ) table_format = context.get(subject_ctx, context.TABLE_FORMAT) or DEFAULT_FORMAT is_structured = table_format in SERIALIZATION_FORMATS # Resolve the column selection: an explicit context entry wins, else a # sibling --columns option, else the provided default. if context.get(subject_ctx, context.COLUMNS) is None: cols_option = search_params(cmd.get_params(subject_ctx), ColumnsOption) if cols_option and isinstance(cols_option, ColumnsOption): cols_value, _ = cols_option.consume_value(subject_ctx, opts) cols_option.init_columns( subject_ctx, cols_option, cols_option.type.convert(cols_value, cols_option, subject_ctx) if cols_value else (), ) selected_ids: tuple[str, ...] = context.get(subject_ctx, context.COLUMNS) or () if not selected_ids and default_columns: selected_ids = tuple(default_columns) if not selected_ids: # No projection asked for: show every column but the opt-in ones, which # would otherwise squeeze all the others (see ColumnSpec.optional). selected_ids = ShowParamsOption.default_column_ids() # Validate the requested IDs against the column registry so unknown IDs # become a clear, actionable UsageError. canonical_ids = ShowParamsOption.column_ids() known_ids = set(canonical_ids) unknown = [col_id for col_id in selected_ids if col_id not in known_ids] if unknown: joined = ", ".join(repr(c) for c in unknown) accepted = ", ".join(canonical_ids) raise click.UsageError( f"Unknown --columns ID(s): {joined}. Accepted: {accepted}.", ctx=subject_ctx, ) table: list[tuple[Any, ...]] = [] for keys, param, owning_ctx in walk_command_params( cmd, subject_ctx, (cmd.name or "",) ): path = PARAM_PATH_SEP.join(keys) param_value, source = get_param_value(param, owning_ctx) # Whether the parameter is reachable from a configuration file. allowed_in_conf_bool = None if config_option: config_option.params_template # noqa: B018 allowed_in_conf_bool = path not in config_option.excluded_params row = format_param_row(param, owning_ctx, path, is_structured) # Config-file provenance: only meaningful when the effective value # came from the default_map; the layer walk then names the file it # resolved from. config_file: Path | URL | str | None = None if source is ParameterSource.DEFAULT_MAP: location = param_config_source(subject_ctx.find_root(), keys) if location is not None: config_file = location if is_structured else shrinkuser(str(location)) if is_structured: param_value = _structured_value(param_value) row["allowed_in_conf"] = allowed_in_conf_bool row["value"] = param_value row["source"] = source.name if source else None row["config_file"] = str(config_file) if config_file else None else: allowed_in_conf = None if allowed_in_conf_bool is not None: allowed_in_conf = ok_styled if allowed_in_conf_bool else ko_styled row["allowed_in_conf"] = allowed_in_conf row["value"] = repr(param_value) row["source"] = source.name if source else None row["config_file"] = config_file table.append(select_row(row, selected_ids, canonical_ids)) def sort_by_depth(line: Sequence[Any]) -> tuple[int, Any]: """Sort by depth first, then path, keeping top-level params on top.""" param_path = line[0] return len(param_path.split(PARAM_PATH_SEP)), param_path selected_columns = select_columns(ShowParamsOption.TABLE_HEADERS, selected_ids) labels = tuple(col.label for col in selected_columns) header_labels: tuple[Any, ...] if is_structured: header_labels = labels else: header_style = Style(bold=True) header_labels = tuple(map(header_style, labels)) print_table( sorted(table, key=sort_by_depth), headers=header_labels, table_format=table_format, )
[docs] class ShowParamsOption(ExtraOption, ParamStructure): """A pre-configured option adding a `--params` option. Between configuration files, default values and environment variables, it might be hard to guess under which set of parameters the CLI will be executed. This option print information about the parameters that will be fed to the CLI. ```{note} The flag is named `--params`, not `--show-params`. It names the view it prints, matching the neighbouring bare-noun informational flags (`--help`, `--version`, `--man`, `--tree`), none of which carry a `show-` verb prefix. The class and {func}`@show_params_option <click_extra.decorators.show_params_option>` decorator keep their historical names: the class is named for what it does (show the parameters), while the flag and the parameter's ID use the bare noun. ``` """ from .table import ColumnSpec as _ColumnSpec TABLE_HEADERS: ClassVar[tuple[_ColumnSpec, ...]] = ( _ColumnSpec( id="id", label="ID", description=( "Fully-qualified parameter path (`cli.subcommand.param_name`) " "derived from the [`click.Command`]" "(https://click.palletsprojects.com/en/stable/api/#click.Command) " "tree. Doubles as the key used to address the parameter from a " "configuration file, which also accepts the kebab-case " "spelling of the last segment." ), ), _ColumnSpec( id="spec", label="Spec.", description=( "Option/argument specification string (like `-v, --verbose`) " "extracted from [`click.Parameter.get_help_record()`]" "(https://click.palletsprojects.com/en/stable/api/" "#click.Parameter)." ), ), _ColumnSpec( id="help", label="Help", optional=True, description=( "The parameter's own help text, as written by the CLI author. " "Opt-in: it is the only column carrying free-form prose, so it " "stays out of the default table and is selected by ID " "(`--columns id,spec,help`). Structured formats are its main " "audience: it turns a `--params` dump into a self-describing " "inventory a tool or an agent can read without also parsing the " "rendered `--help` screen." ), ), _ColumnSpec( id="class", label="Class", description=( "Fully-qualified class of the parameter: a subclass of " "[`click.Option`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option), " "[`click.Argument`]" "(https://click.palletsprojects.com/en/stable/api/#click.Argument), " "[`cloup.Option`]" "(https://cloup.readthedocs.io/en/stable/autoapi/cloup/" "index.html#cloup.Option), " "or one of Click Extra's own wrappers " "([`click_extra.parameters.Option`](#click_extra.parameters.Option), " "[`click_extra.parameters.Argument`]" "(#click_extra.parameters.Argument), " "[`click_extra.parameters.ExtraOption`]" "(#click_extra.parameters.ExtraOption))." ), ), _ColumnSpec( id="param_type", label="Param type", description=( "Click value converter class: a subclass of [`click.ParamType`]" "(https://click.palletsprojects.com/en/stable/api/" "#click.ParamType) like [`click.IntRange`]" "(https://click.palletsprojects.com/en/stable/api/#click.IntRange), " "[`click.Choice`]" "(https://click.palletsprojects.com/en/stable/api/#click.Choice), " "or a Click Extra type." ), ), _ColumnSpec( id="python_type", label="Python type", description=( "Python built-in type the parsed value resolves to: " "[`str`](https://docs.python.org/3/library/stdtypes.html" "#text-sequence-type-str), " "[`int`](https://docs.python.org/3/library/functions.html#int), " "[`float`](https://docs.python.org/3/library/functions.html#float), " "[`bool`](https://docs.python.org/3/library/functions.html#bool), " "or [`list`](https://docs.python.org/3/library/stdtypes.html#list). " "Computed by [`ParamStructure.get_param_type()`]" "(#click_extra.parameters.ParamStructure.get_param_type) from " "the Click `Param type`." ), ), _ColumnSpec( id="hidden", label="Hidden", description=( "Reflects [`click.Option`'s `hidden`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option) " "constructor argument: the option is omitted from `--help` output. " "Empty for [`click.Argument`]" "(https://click.palletsprojects.com/en/stable/api/#click.Argument), " "which does not support hiding." ), ), _ColumnSpec( id="exposed", label="Exposed", description=( "Reflects [`click.Parameter`'s `expose_value`]" "(https://click.palletsprojects.com/en/stable/api/#click.Parameter) " "constructor argument: whether the parsed value is forwarded to " "the command callback. Eager options like `--params` and " "`--help` typically run a callback and exit, so they are not " "exposed." ), ), _ColumnSpec( id="allowed_in_conf", label="Allowed in conf?", description=( "Click Extra-specific: whether the parameter is reachable from a " "configuration file. Controlled by [`ParamStructure.excluded_params`]" "(#click_extra.parameters.ParamStructure.excluded_params) and " "[`included_params`]" "(#click_extra.parameters.ParamStructure.included_params). Empty " "when the CLI has no [`--config` option](config.md)." ), ), _ColumnSpec( id="envvars", label="Env. vars.", description=( "Environment variables read for this parameter: the explicit " "[`click.Parameter`'s `envvar`]" "(https://click.palletsprojects.com/en/stable/api/#click.Parameter) " "plus the auto-resolved IDs documented in " "[Environment variables](envvar.md)." ), ), _ColumnSpec( id="default", label="Default", description=( "Default value returned by [`click.Parameter.get_default()`]" "(https://click.palletsprojects.com/en/stable/api/" "#click.Parameter.get_default), rendered as its Python `repr()`." ), ), _ColumnSpec( id="is_flag", label="Is flag", description=( "Reflects [`click.Option`'s `is_flag`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option): " "whether the option behaves as a flag (no value taken from the " "command line). Empty for [`click.Argument`]" "(https://click.palletsprojects.com/en/stable/api/#click.Argument)." ), ), _ColumnSpec( id="flag_value", label="Flag value", description=( "Reflects [`click.Option`'s `flag_value`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option): " "the Python value substituted for the option when its flag is " "used. Defaults to `True` for boolean flags, can be any value " "for flag-value style options " "(like `@option('--upper', 'transform', flag_value='upper')`)." ), ), _ColumnSpec( id="is_bool_flag", label="Is bool flag", description=( "Reflects `click.Option.is_bool_flag` (set internally by Click " "when `flag_value` is `True` or `False`): the option is a *true* " "boolean flag, as opposed to a flag-value style option." ), ), _ColumnSpec( id="multiple", label="Multiple", description=( "Reflects [`click.Parameter`'s `multiple`]" "(https://click.palletsprojects.com/en/stable/api/#click.Parameter): " "the parameter can be repeated on the command line, collecting " "values into a tuple." ), ), _ColumnSpec( id="nargs", label="Nargs", description=( "Reflects [`click.Parameter`'s `nargs`]" "(https://click.palletsprojects.com/en/stable/api/#click.Parameter): " "the number of CLI tokens the parameter consumes. `1` is the " "default; `-1` denotes a variadic argument." ), ), _ColumnSpec( id="prompt", label="Prompt", description=( "Reflects [`click.Option`'s `prompt`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option): " "the text shown to the user when the option is not provided on " "the command line. Empty when no prompt is configured." ), ), _ColumnSpec( id="confirmation_prompt", label="Confirmation prompt", description=( "Reflects [`click.Option`'s `confirmation_prompt`]" "(https://click.palletsprojects.com/en/stable/api/#click.Option): " "whether the user is asked to enter the value twice for " "confirmation." ), ), _ColumnSpec( id="value", label="Value", description=( "Current value of the parameter at invocation time, computed by " "[`click.Parameter.consume_value()`]" "(https://click.palletsprojects.com/en/stable/api/#click.Parameter) " "from the merged sources (CLI, environment, config file, default)." ), ), _ColumnSpec( id="source", label="Source", description=( "Provenance of the resolved value: a [`click.core.ParameterSource`]" "(https://click.palletsprojects.com/en/stable/api/" "#click.core.ParameterSource) enum member such as `COMMANDLINE`, " "`ENVIRONMENT`, `DEFAULT_MAP`, or `DEFAULT`." ), ), _ColumnSpec( id="config_file", label="Config file", optional=True, description=( "The configuration file the effective value was loaded from, " "when `Source` reports `DEFAULT_MAP`. With " "[`cascade=True`](config.md#cascading-configuration-files) " "several files are layered and this column names the one that " "won the parameter; with a single loaded file, every " "config-sourced parameter names that file. Empty for every " "other source and when no configuration file was loaded. " "Opt-in, like `help`: paths are wide and stay redundant with " "`Source` until several files take part." ), ), ) """Rich column registry for the `--params` table. Each entry is a {class}`click_extra.table.ColumnSpec` carrying the column's stable `id` (used by `--columns` and as structured-format key), its display `label`, and a MyST/Markdown `description` consumed by the documentation's auto-generated *Available columns* section. Iteration yields columns in canonical display order. """
[docs] @classmethod def column_labels(cls) -> tuple[str, ...]: """Return just the display labels of {data}`TABLE_HEADERS` (in order).""" return tuple(col.label for col in cls.TABLE_HEADERS)
[docs] @classmethod def column_ids(cls) -> tuple[str, ...]: """Return just the stable IDs of {data}`TABLE_HEADERS` (in order).""" return tuple(col.id for col in cls.TABLE_HEADERS)
[docs] @classmethod def default_columns(cls) -> tuple[_ColumnSpec, ...]: """Return the columns rendered when `--columns` asks for no projection. Every column but the {attr}`~click_extra.table.ColumnSpec.optional` ones, which stay addressable by ID and out of the way until named. """ return tuple(col for col in cls.TABLE_HEADERS if not col.optional)
[docs] @classmethod def default_column_ids(cls) -> tuple[str, ...]: """Return the stable IDs of {meth}`default_columns` (in order).""" return tuple(col.id for col in cls.default_columns())
[docs] @classmethod def default_column_labels(cls) -> tuple[str, ...]: """Return the display labels of {meth}`default_columns` (in order).""" return tuple(col.label for col in cls.default_columns())
[docs] @classmethod def find_column(cls, column_id: str): """Return the {class}`~click_extra.table.ColumnSpec` matching `column_id`. Raises `KeyError` if no column has this ID; callers should convert the error into a {class}`click.UsageError` when surfaced to a user. """ for col in cls.TABLE_HEADERS: if col.id == column_id: return col msg = f"Unknown column ID {column_id!r}" raise KeyError(msg)
[docs] @classmethod def render_doc_table(cls) -> str: """Render {data}`TABLE_HEADERS` as a Markdown table for documentation. Used by the `show_params_columns_table` MyST substitution in `docs/conf.py` to feed the *Available columns* section of `docs/parameters.md`: editing a description here automatically rebuilds the docs table on the next `sphinx-build`. """ # Imported here to avoid a circular import: table imports from this module. from .table import render_columns_markdown_table return render_columns_markdown_table(cls.TABLE_HEADERS)
def __init__( self, param_decls: Sequence[str] | None = None, is_flag=True, expose_value=False, is_eager=True, help=_( "Show all CLI parameters, their provenance, defaults and value, then exit.", ), **kwargs, ) -> None: if not param_decls: param_decls = ("--params",) kwargs.setdefault("callback", self.print_params) self.excluded_params = frozenset() """Deactivates the blocking of any parameter.""" self.included_params = None """No allowlist filter; show all parameters.""" super().__init__( param_decls=param_decls, is_flag=is_flag, expose_value=expose_value, is_eager=is_eager, help=help, **kwargs, )
[docs] def print_params( self, ctx: click.Context, param: click.Parameter, value: bool, ) -> None: """Introspect the current CLI and print its parameter metadata table. Thin wrapper over {func}`~click_extra.parameters.render_params_table`, the shared rendering core also driving `click-extra wrap --params` for foreign CLIs. The live invocation context carries everything the core needs: the captured {data}`~click_extra.context.RAW_ARGS` (attached by `Command`/`Group`) for value and source resolution, plus any sibling `--table-format` / `--columns` options. """ # Exit early if the callback was processed but the option wasn't set. if not value: return # Load the configuration file first, so the value and source columns # reflect it even when this flag was processed ahead of the --config # option. Imported here to avoid a circular import with the config # module, which imports from this one. from .config import ensure_config_loaded ensure_config_loaded(ctx) # Warn when the live command is not a Command: without the captured # raw arguments, the value/source columns fall back to defaults. if context.get(ctx, context.RAW_ARGS) is None: logger.warning( f"Cannot extract parameters values: " f"{ctx.command} does not inherits from Command.", ) render_params_table(ctx) ctx.exit()
def __getattr__(name: str) -> Any: """Resolve deprecated `parameters` symbols via the PEP 562 `__getattr__` hook. The generic helpers (`patch_attr`, `generator_tag`, `missing_extra_message`) moved to {mod}`click_extra._utils`. Fires only for names not defined in this module. See {mod}`click_extra._deprecated`. """ from ._deprecated import resolve_deprecated return resolve_deprecated(__name__, name)