diff --git a/CHANGES.rst b/CHANGES.rst index b5d970117..c2d814223 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,6 +31,8 @@ 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`` and ``make_dynamic_context`` + classmethod to ``Command``. :pr:`2784` Version 8.1.8 diff --git a/src/click/core.py b/src/click/core.py index cc0d4603b..04715a00d 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -227,6 +227,9 @@ class Context: context. ``Command.show_default`` overrides this default for the specific command. + .. versionchanged:: 8.2 + Added the ``dynamic_params`` attribute. + .. versionchanged:: 8.2 The ``protected_args`` attribute is deprecated and will be removed in Click 9.0. ``args`` will contain remaining unparsed tokens. @@ -425,6 +428,12 @@ def __init__( #: Show option default values when formatting help text. self.show_default: bool | None = show_default + #: A list of :class:`Parameter` objects that the attached command will + #: use. These will be stored in the :attr:`args` attribute if both the + #: :attr:`allow_extra_args` and :attr:`ignore_unknown_options` flags + #: are set to ``True``. + self.dynamic_params: list[Parameter] = [] + self._close_callbacks: list[t.Callable[[], t.Any]] = [] self._depth = 0 self._parameter_source: dict[str, ParameterSource] = {} @@ -951,11 +960,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 @@ -1131,6 +1140,22 @@ def make_context( self.parse_args(ctx, args) return ctx + @classmethod + def make_dynamic_context(cls, ctx: Context, **extra: t.Any) -> Context: + """This function creates a new context based on the given context's + :attr:`Context.dynamic_params` attribute. This is useful for parsing the + collected dynamic arguments using the new context's :attr:`Context.params`. + + :param ctx: the context to inherit dynamic parameters from. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.2 + Added the ``make_dynamic_context`` classmethod. + """ + cmd = cls(name=None, params=ctx.dynamic_params) + return cmd.make_context(info_name=None, args=ctx.args, parent=ctx, **extra) + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: if not args and self.no_args_is_help and not ctx.resilient_parsing: echo(ctx.get_help(), color=ctx.color) diff --git a/tests/test_commands.py b/tests/test_commands.py index 5a56799ad..21ecfe309 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -409,3 +409,38 @@ 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( + context_settings={"allow_extra_args": True, "ignore_unknown_options": True} + ) + @click.option("--dyn", required=True, is_eager=True, callback=callback) + @click.pass_context + def command(ctx, dyn): + dynamic_context = command.make_dynamic_context(ctx) + assert dynamic_context.params == {dyn: "bar"} + + rv = runner.invoke(command, ["--dyn", "foo", "--foo", "bar"]) + assert rv.exit_code == 0, rv.output + + +def test_dynamic_params_help(runner): + def callback(ctx, p, v): + ctx.dynamic_params.append(click.Option([f"--{v}"])) + return v + + @click.command( + context_settings={"allow_extra_args": True, "ignore_unknown_options": True} + ) + @click.option("--dyn", default="foo", is_eager=True, callback=callback) + @click.pass_context + def command(ctx, dyn): + pass + + rv = runner.invoke(command, ["--help"]) + assert "--foo" in rv.output, rv.output