From 390192577ac9ea3e2e3d355c55f594721b49fe57 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 28 Sep 2024 14:02:28 -0400 Subject: [PATCH] Allow adding dynamic parameters to every Context --- CHANGES.rst | 1 + src/click/core.py | 18 +++++++++++++++--- tests/test_commands.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b5d970117..b7f5dafda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,7 @@ Unreleased - When generating a command's name from a decorated function's name, the suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. :issue:`2322` +- Add ``dynamic_params`` property to ``Context``. :pr:`2784` Version 8.1.8 diff --git a/src/click/core.py b/src/click/core.py index cc0d4603b..acb016ced 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -226,6 +226,11 @@ class Context: value is not set, it defaults to the value from the parent context. ``Command.show_default`` overrides this default for the specific command. + :param dynamic_params: A list of :class:`Parameter` objects that the + attached command will use. + + .. versionchanged:: 8.2 + Added the ``dynamic_params`` parameter. .. versionchanged:: 8.2 The ``protected_args`` attribute is deprecated and will be removed in @@ -278,6 +283,7 @@ def __init__( token_normalize_func: t.Callable[[str], str] | None = None, color: bool | None = None, show_default: bool | None = None, + dynamic_params: list[Parameter] | None = None, ) -> None: #: the parent context or `None` if none exists. self.parent = parent @@ -425,6 +431,12 @@ def __init__( #: Show option default values when formatting help text. self.show_default: bool | None = show_default + if dynamic_params is None: + dynamic_params = [] + + #: Allow for dynamic parameters. + self.dynamic_params: list[Parameter] = dynamic_params + self._close_callbacks: list[t.Callable[[], t.Any]] = [] self._depth = 0 self._parameter_source: dict[str, ParameterSource] = {} @@ -951,11 +963,11 @@ def get_usage(self, ctx: Context) -> str: return formatter.getvalue().rstrip("\n") def get_params(self, ctx: Context) -> list[Parameter]: - rv = self.params - help_option = self.get_help_option(ctx) + rv = [*self.params, *ctx.dynamic_params] + help_option = self.get_help_option(ctx) if help_option is not None: - rv = [*rv, help_option] + rv.append(help_option) return rv diff --git a/tests/test_commands.py b/tests/test_commands.py index 5a56799ad..5154972ed 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -409,3 +409,17 @@ def cli(): assert rv.exit_code == 1 assert isinstance(rv.exception.__cause__, exc) assert rv.exception.__cause__.args == ("catch me!",) + + +def test_dynamic_params(runner): + def callback(ctx, p, v): + ctx.dynamic_params.append(click.Option([f"--{v}"])) + return v + + @click.command() + @click.option("--dyn", required=True, is_eager=True, callback=callback) + def command(dyn, **kwargs): + assert dyn in kwargs + assert kwargs[dyn] == "bar" + + runner.invoke(command, ["--dyn", "foo", "--foo", "bar"])