Source code for click_extra.param_tools

# 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.
"""Helpers to introspect a CLI: its parameters and its command tree.

Every renderer of a CLI reads it through these helpers: the man pages, the
Markdown and JSON documents, the Carapace completion spec, the `--tree` view and
the `--params` table. One helper per question keeps their answers from drifting
apart: which parameters a command lists and in which order, what each one is
called, what it accepts, and how its help text reads once Click's markers are
taken out.
"""

from __future__ import annotations

import inspect
import logging
import re
from enum import Enum
from functools import cache, cached_property, reduce
from itertools import chain
from operator import getitem
from typing import TypeVar

import click
import cloup
from click import ParamType, get_current_context
from click.core import _format_deprecated_label

from . import context
from ._utils import set_nested

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

logger = logging.getLogger(__name__)

P = TypeVar("P", bound=click.Parameter)
"""Type variable bound to {class}`click.Parameter`, letting {func}`search_params`,
{func}`last_param` and {func}`require_sibling_param` return the exact subclass
they were asked to find."""

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

INLINE_LITERAL_RE = re.compile(r"``([^`]+?)``")
"""Match a reST inline literal, text between two pairs of backticks, in a docstring.

Click stores docstrings verbatim, so any reST markup the author used to
render code-like tokens in HTML docs leaks into `Command.help` /
`Command.short_help`. The roff and HTML man-page paths translate these
matches into the bold/literal markers their renderers understand; the
Sphinx index directive translates them into `nodes.literal`.
"""


[docs] def iter_inline_literals(text: str) -> Iterator[tuple[str, bool]]: """Walk `text` and yield `(segment, is_literal)` pairs. Split on {data}`INLINE_LITERAL_RE` so the consumer can apply different rendering to the literal segments (bold for roff, a `literal` node for docutils) without re-parsing the regex. """ pos = 0 for match in INLINE_LITERAL_RE.finditer(text): if match.start() > pos: yield text[pos : match.start()], False yield match.group(1), True pos = match.end() if pos < len(text): yield text[pos:], False
[docs] def clean_help(text: str) -> str: r"""Normalize Click help prose for the backends that carry newlines natively. Runs {func}`inspect.cleandoc` and drops Click's `\b` (`\x08`) no-rewrap marker, keeping every line break the marker protected. Markdown, JSON and a completion spec all represent those breaks on their own, so none needs an equivalent of the roff `.nf` / `.fi` pair: only the control character has to go, or it lands in the output as a stray byte. """ return inspect.cleandoc(text).replace("\x08\n", "").replace("\x08", "").strip()
[docs] def choice_values(param_type: click.ParamType) -> tuple[str, ...]: """The values a `click.Choice`-like type accepts, spelled as Click completes them. An {class}`~enum.Enum` member is named by its `name`, as {meth}`click.Choice.normalize_choice` does before it folds case, and anything else by `str()`. A type carrying no `choices` attribute, or an empty one, answers an empty tuple. The one spelling every document of a CLI lists a choice under: the help screen, the man page, the completion spec. """ choices = getattr(param_type, "choices", None) if not choices: return () return tuple( choice.name if isinstance(choice, Enum) else str(choice) for choice in choices )
[docs] def search_params( params: Iterable[click.Parameter], klass: type[P], include_subclasses: bool = True, ) -> P | None: """Return the one parameter of class `klass` in `params`, or `None`. The result is typed as `klass` itself, so a caller needs no `isinstance` check to use what it found. :param params: list of parameter instances to search in. :param klass: the class of the parameter to look for. :param include_subclasses: if `True` (the default), a parameter subclassing `klass` matches too. If `False`, only a parameter of exactly `klass` does. :raises RuntimeError: if more than one parameter matches. """ param_list = [ p for p in params if isinstance(p, klass) and (include_subclasses or p.__class__ is klass) ] if not param_list: return None if len(param_list) != 1: raise RuntimeError( f"More than one {klass.__name__} parameter found on command: {param_list}" ) return param_list[0]
[docs] def last_param( params: Iterable[click.Parameter], klass: type[P], ) -> P | 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 = [p for p in params if isinstance(p, klass) and p.__class__ is klass] return options[-1] if options else None
[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 sibling is None: raise RuntimeError( 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 appends Click's own label, so a listing reads the same marker Click's `get_short_help_str()` produces, reason string included. """ 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, drop Click's `\b` # no-rewrap marker as `get_short_help_str()` does, then squash internal # newlines so the result is one line. paragraph = command.help.split("\n\n", 1)[0] text = clean_help(paragraph).replace("\n", " ") else: text = "" if command.deprecated: label = _format_deprecated_label(command.deprecated) text = f"{text} {label}".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 `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. """ if param.help: return param.help 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 canonical_param_name(name: str) -> str: """Fold a spelling to the parameter name Click would derive from it. Applies Click's own derivation, minus the option prefix its callers strip first: hyphens become underscores and the result is case-folded, so `Foo-Bar` answers `foo_bar`. Use it to resolve an outside spelling (a configuration key, an argfile declaration) against the names a CLI already has. ```{caution} The fold is many-to-one, so it identifies a name and never reconstructs a spelling. `--foo-bar`, `--Foo-Bar` and `--FOO-BAR` answer the same name, as do `--foo--bar` and `--foo__bar`, and `--K` (the Kelvin sign, `U+212A`) lands on the same `k` as `--k`. Case folding reaches every script and is not length-preserving: `--ẞ` answers `ß`, whose upper case is the two letters `SS`, and a composed `café` answers a different name than a decomposed one that renders identically. ``` ```{caution} A folded spelling is a *candidate*, never an answer on its own. Click takes an identifier declaration verbatim, so `@option("--x", "Foo_Bar")` names its parameter `Foo_Bar`, which this function would never produce. Match the fold against the names a CLI declares and keep the CLI's own spelling, rather than storing what this returns. ``` ```{todo} Upstream [pallets/click#3827](https://github.com/pallets/click/pull/3827) asks whether `Option` should keep the case of an identifier declaration, as it does today, or fold it the way `Argument` already does. Should Click fold both, no parameter can be named `Foo_Bar` any more: drop the caution above, and with it the ambiguity branch of `_merge_into_template` in `click_extra.config.schema`, which exists only to tell two such names apart. ``` """ return name.replace("-", "_").lower()
[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 LOCATION`). ```{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) or getattr(param, "secondary_opts", None) ): return "flag" if resolve_flag_value(param) is not None: return "optional" return "required"
[docs] def resolve_flag_value(param: click.Parameter) -> Any: """The value `param`'s primary declaration stands for. `None` for a plain option, a counter or an argument, `True` for a boolean flag, and the declared value otherwise (`--color` standing for `always`). Read from Click's `Option.flag_activation_value`, not from `flag_value`: Click keeps the `UNSET` sentinel there when no value was declared, and the sentinel stored anywhere it is read back as a value turns the flag off. """ return getattr(param, "flag_activation_value", None)
[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))
[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}`~click_extra.show_params.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. With an allowlist, this holds only the IDs that the walk of {meth}`~click_extra.param_tools.ParamStructure.build_param_trees` reaches and the allowlist leaves out. Ask {meth}`~click_extra.param_tools.ParamStructure.is_excluded` about a parameter of another subcommand. """ included_params: frozenset[str] | None """Allowlist of parameter IDs, mutually exclusive with `excluded_params`. `None` disables the allowlist. Otherwise {meth}`~click_extra.param_tools.ParamStructure.build_param_trees` walks only the subcommands an allowed ID lies under, and resolves the parameters it reaches into `excluded_params`. """
[docs] @staticmethod def init_tree_dict(*path: str, leaf: Any = None) -> Any: """Nest one dict per key of `path`, the deepest one holding `leaf`. An empty `path` returns `leaf` itself. """ return reduce(lambda node, key: {key: node}, reversed(path), leaf)
[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, *, descend: Callable[[tuple[str, ...]], bool] | None = None, ) -> Iterator[tuple[tuple[str, ...], click.Parameter]]: """Yield every parameter of the CLI, unfiltered. Everything is included, from top-level groups to subcommands, and from options to arguments, unless `descend` skips a subcommand. Thin adapter over {func}`~click_extra.param_tools.walk_command_params`: it resolves the root CLI from the active context and drops the per-parameter context that the free function also yields. :param descend: passed on to {func}`~click_extra.param_tools.walk_command_params`, to skip the subcommands it declines. :return: `(keys, param)` pairs: the keys leading to the parameter, then the parameter itself. """ 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,), descend=descend ): 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). """ return _python_type_of(type(click_type))
[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. A boolean flag is a `bool` even when it is repeatable: it collects occurrences of a toggle rather than values, and a configuration file spells it as the boolean it is. That is why the check comes before the multi-value one, which would otherwise read it as a list and have an unset one dump as `[]`, a shape no user writes. See the list of [custom types provided by Click](https://click.palletsprojects.com/en/stable/api/#types). """ if getattr(param, "is_bool_flag", False): return bool if param.multiple or param.nargs != 1: return list return ParamStructure.map_click_type(param.type)
[docs] def is_excluded(self, param_id: str) -> bool: """Whether a configuration file cannot set the parameter `param_id`. An allowlist answers alone: the parameter is excluded when `included_params` does not name it. So the answer holds for a parameter of a subcommand that the walk of {meth}`~click_extra.param_tools.ParamStructure.build_param_trees` skipped. Without an allowlist, the parameter is excluded when `excluded_params` lists it. :param param_id: the fully-qualified ID of the parameter, its path keys joined with {data}`~click_extra.param_tools.PARAM_PATH_SEP`. """ if self.included_params is not None: return param_id not in self.included_params return param_id in self.excluded_params
def _leads_to_included(self, keys: tuple[str, ...]) -> bool: """Whether an ID of `included_params` lies under the subcommand at `keys`.""" assert self.included_params is not None prefix = f"{PARAM_PATH_SEP.join(keys)}{PARAM_PATH_SEP}" return any(param_id.startswith(prefix) for param_id in self.included_params)
[docs] def build_param_trees(self) -> dict[str, Any]: """Build and return the parameters tree structure. Each leaf is the list of the parameters its path names: one, or several when options share a name, like the feature switches of one destination. This removes parameters whose fully-qualified IDs are in the `excluded_params` blocklist. With an allowlist, the tree holds only the parameters `included_params` names. The walk then skips each subcommand that no allowed ID lies under, so it does not import a lazy subcommand of a {class}`~click_extra.commands.LazyGroup` that the allowlist leaves out. An empty allowlist walks the root command alone. The parameters the walk reaches and the allowlist leaves out become `excluded_params`. """ descend = self._leads_to_included if self.included_params is not None else None objects: dict[str, Any] = {} reached_ids = set() for keys, param in self.walk_params(descend=descend): param_id = PARAM_PATH_SEP.join(keys) reached_ids.add(param_id) if self.is_excluded(param_id): continue # Parameters sharing a name, like the feature switches of a single # destination, gather in one leaf, in declaration order. try: leaf = self.get_tree_value(objects, *keys) except (KeyError, TypeError): leaf = None set_nested( objects, keys, [*leaf, param] if isinstance(leaf, list) else [param] ) if self.included_params is not None: self.excluded_params = frozenset(reached_ids) - self.included_params 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()
@cache def _python_type_of( click_type_class: type[ParamType], ) -> type[str | int | float | bool | list]: """The Python type a class of Click parameter type converts to. Answers from {attr}`ParamStructure.TYPE_MAP`: the entry of the class itself, else the first entry the class inherits, else `str`, which is what the command line hands a custom type. Cached by class, since a CLI declares a handful of types and the `--params` table asks once per parameter. """ py_type = ParamStructure.TYPE_MAP.get(click_type_class) if py_type is not None: return py_type for param_type, mapped_type in ParamStructure.TYPE_MAP.items(): if issubclass(click_type_class, param_type): return mapped_type return str
[docs] def get_param_spec(param: click.Parameter, ctx: click.Context) -> str | None: """Extract the option-spec string (like `-v, --verbose`) from a parameter. A hidden option has one too: `click.Option.get_help_spec()` ignores `hidden`. An argument has no spec, so it gets `None`. """ if not isinstance(param, click.Option): return None return param.get_help_spec(ctx)
[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. :param command: the command to build the context of. :param info_name: the name the command goes by in that context. Defaults to the command's own name. :param parent: the context of the command's parent, if any. """ if info_name is None: info_name = command.name return command.make_context(info_name, [], parent=parent, resilient_parsing=True)
[docs] def child_context( command: click.Command, parent: click.Context, info_name: str, ) -> click.Context: """Build the context of a subcommand under `parent`, without parsing anything. Built the way {meth}`click.Command.make_context` builds one, from the command's own `context_class` and `context_settings`, so the context reads like the one a real invocation gets: a subcommand setting its own `auto_envvar_prefix` names its environment variables after it, where a bare `click.Context` would derive them from the parent's prefix. Unlike {func}`make_resilient_context`, no parameter is processed, so a walk over a live invocation never re-runs a callback. """ return command.context_class( command, info_name=info_name, parent=parent, **command.context_settings, )
[docs] def iter_subcommands( command: click.Command, ctx: click.Context, *, skip_hidden: bool = True, listing: bool = False, ) -> 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). :param listing: resolve each name to the command the group's help screen describes, through its `listed_command` method when it has one. A {class}`~click_extra.commands.LazyGroup` answers there with a stand-in for a lazy subcommand declaring its help, so the walk imports nothing. """ if not isinstance(command, click.Group): return resolve = command.get_command if listing: resolve = getattr(command, "listed_command", resolve) for name in command.list_commands(ctx): sub = resolve(ctx, name) if sub is None: continue if skip_hidden and getattr(sub, "hidden", False): continue yield name, sub
[docs] def iter_subcommand_contexts( command: click.Command, ctx: click.Context, *, skip_hidden: bool = True, ) -> Iterator[tuple[str, click.Command, click.Context]]: """Yield a group's direct subcommands, each with an introspection context. Pairs {func}`iter_subcommands` with {func}`make_resilient_context`: every renderer walking a command tree (the man pages, the Markdown and JSON documents, the Carapace spec, the `--tree` view) descends through here, so a lazy subcommand, the order {meth}`click.Group.list_commands` sets and a name resolving to `None` are handled the same way in all of them. Each context is built under `ctx` and named after the subcommand's registration name, so its `command_path` and its environment variable prefix are the ones a run reaches. :param skip_hidden: passed on to {func}`iter_subcommands`. :return: `(name, subcommand, subcontext)` triples, in listing order. """ for name, sub in iter_subcommands(command, ctx, skip_hidden=skip_hidden): yield name, sub, make_resilient_context(sub, name, parent=ctx)
[docs] def split_option_groups( command: cloup.OptionGroupMixin, ) -> tuple[tuple[cloup.OptionGroup, ...], tuple[cloup.OptionGroup, ...]]: """A command's explicit option groups, split around its ungrouped section. Hands back the groups drawn *before* the ungrouped options, then those drawn *after*. A Click Extra command decides that split itself, through {meth}`click_extra.commands.Command.split_option_groups`, so its own sections land past the ungrouped ones. A plain Cloup command has no such method and keeps every group ahead of the ungrouped section, which is Cloup's own rule. Every renderer reading a command's sections goes through this, so the split is resolved in one place whatever the command class. :param command: the command whose groups are read. :return: the groups drawn before the ungrouped section, then those after it. """ splitter = getattr(command, "split_option_groups", None) if splitter is None: return tuple(command.option_groups), () return splitter() # type: ignore[no-any-return]
[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`, sectioned by {meth}`click_extra.commands.Command.split_option_groups` and sorted within a section by {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 own_groups, extra_groups = split_option_groups(command) seen: set[int] = set() for param in chain( command.arguments, (option for group in own_groups for option in group.options), command.get_ungrouped_options(ctx), (option for group in extra_groups for option in group.options), # A parameter appended to `command.params` after construction never made # it into the cached groups: yield it rather than drop it from the render. 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, ...] = (), *, descend: Callable[[tuple[str, ...]], bool] | None = None, ) -> 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 (see {func}`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, with the subcommand's own context settings applied, 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. :param descend: called with the path keys of each subcommand, before the group resolves it. The walk skips a subcommand for which it returns `False`, so a lazy subcommand it declines is not imported. `None` walks every subcommand. """ 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_keys = (*parent_keys, subcmd_name) if descend is not None and not descend(subcmd_keys): continue subcmd = cmd.get_command(ctx, subcmd_name) if subcmd is None: continue subcmd_ctx = child_context(subcmd, ctx, subcmd_name) yield from walk_command_params( subcmd, subcmd_ctx, subcmd_keys, descend=descend )
[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