From 390192577ac9ea3e2e3d355c55f594721b49fe57 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 28 Sep 2024 14:02:28 -0400 Subject: [PATCH 1/6] 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"]) From d7ae4c78199ecee78062f2a72c007748f9134467 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 19 Oct 2024 12:08:27 -0400 Subject: [PATCH 2/6] fix everything --- src/click/core.py | 20 +++++++++++++++++++- tests/test_commands.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index acb016ced..d6a64acc2 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -434,7 +434,9 @@ def __init__( if dynamic_params is None: dynamic_params = [] - #: Allow for dynamic parameters. + #: Allow for dynamic parameters. These will be stored in the + #: :attr:`args` attribute if both :attr:`Context.allow_extra_args` and + #: :attr:`ignore_unknown_options` flags are set to ``True``. self.dynamic_params: list[Parameter] = dynamic_params self._close_callbacks: list[t.Callable[[], t.Any]] = [] @@ -1143,6 +1145,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. + """ + 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 5154972ed..21ecfe309 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -416,10 +416,31 @@ def callback(ctx, p, v): ctx.dynamic_params.append(click.Option([f"--{v}"])) return v - @click.command() + @click.command( + context_settings={"allow_extra_args": True, "ignore_unknown_options": True} + ) @click.option("--dyn", required=True, is_eager=True, callback=callback) - def command(dyn, **kwargs): - assert dyn in kwargs - assert kwargs[dyn] == "bar" + @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 - runner.invoke(command, ["--dyn", "foo", "--foo", "bar"]) + rv = runner.invoke(command, ["--help"]) + assert "--foo" in rv.output, rv.output From 8f365262e582ed56204c1670ca29eafc05487ed4 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 19 Oct 2024 12:13:18 -0400 Subject: [PATCH 3/6] Update CHANGES.rst --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b7f5dafda..c2d814223 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,7 +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``. :pr:`2784` +- Add ``dynamic_params`` property to ``Context`` and ``make_dynamic_context`` + classmethod to ``Command``. :pr:`2784` Version 8.1.8 From 202adb272a5f865b7bae164e208ddea8f13a8424 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 19 Oct 2024 12:21:27 -0400 Subject: [PATCH 4/6] remove superfluous parameter --- src/click/core.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index d6a64acc2..e2e71b31e 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -226,11 +226,9 @@ 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. + Added the ``dynamic_params`` attribute. .. versionchanged:: 8.2 The ``protected_args`` attribute is deprecated and will be removed in @@ -283,7 +281,6 @@ 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 @@ -431,13 +428,11 @@ 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. These will be stored in the - #: :attr:`args` attribute if both :attr:`Context.allow_extra_args` and - #: :attr:`ignore_unknown_options` flags are set to ``True``. - self.dynamic_params: list[Parameter] = dynamic_params + #: A list of :class:`Parameter` objects that the attached command will + #: use. These will be stored in the :attr:`args` attribute if both + #: :attr:`Context.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 From c4e475ed8a49df29d9abbd3b4debc3b1f8bc3582 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 19 Oct 2024 12:23:30 -0400 Subject: [PATCH 5/6] fix comment typo --- src/click/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index e2e71b31e..168f21871 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -429,9 +429,9 @@ def __init__( 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 - #: :attr:`Context.allow_extra_args` and :attr:`ignore_unknown_options` - #: flags are set to ``True``. + #: 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]] = [] From 77c9842df90ddd5f23371a99464e25ad77b3145a Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 19 Oct 2024 12:26:23 -0400 Subject: [PATCH 6/6] improve the versionchanged note --- src/click/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/click/core.py b/src/click/core.py index 168f21871..04715a00d 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1151,7 +1151,7 @@ def make_dynamic_context(cls, ctx: Context, **extra: t.Any) -> Context: constructor. .. versionchanged:: 8.2 - Added. + 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)