From 5fc3423ae54990cec5a8b41d2c7b489da41475b0 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Fri, 19 Sep 2025 14:59:07 +0200 Subject: [PATCH 1/2] Hide UNSET values in Context.invoke --- CHANGES.rst | 4 ++-- src/click/core.py | 12 ++++++++++- tests/test_commands.py | 46 +++++++++++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 822510664..602aa22c0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,8 +6,8 @@ Version 8.3.x Unreleased - Don't discard pager arguments by correctly using subprocess.Popen. :issue:`3039` :pr:`3055` - - +- Replace ``Sentinel.UNSET`` default values by ``None`` as they're passed through + the ``Context.invoke()`` method. :issue:`3066` :issue:`3065` :pr:`3068` Version 8.3.0 -------------- diff --git a/src/click/core.py b/src/click/core.py index ff2f74ad5..cd8052f7d 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -799,8 +799,18 @@ def invoke( for param in other_cmd.params: if param.name not in kwargs and param.expose_value: + default_value = param.get_default(ctx) + # We explicitly hide the :attr:`UNSET` value to the user, as we + # choose to make it an implementation detail. And because ``invoke`` + # has been designed as part of Click public API, we return ``None`` + # instead. Refs: + # https://github.com/pallets/click/issues/3066 + # https://github.com/pallets/click/issues/3065 + # https://github.com/pallets/click/pull/3068 + if default_value is UNSET: + default_value = None kwargs[param.name] = param.type_cast_value( # type: ignore - ctx, param.get_default(ctx) + ctx, default_value ) # Track all kwargs as params, so that forward() will pass diff --git a/tests/test_commands.py b/tests/test_commands.py index a5aa43f8a..f26529a54 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -228,24 +228,52 @@ def sync(ctx): assert result.output == "Debug is off\n" -def test_other_command_invoke_with_defaults(runner): +@pytest.mark.parametrize( + ("opt_params", "expected"), + ( + # Original tests. + ({"type": click.INT, "default": 42}, 42), + ({"type": click.INT, "default": "15"}, 15), + ({"multiple": True}, ()), + # SENTINEL value tests. + ({"default": None}, None), + ({"type": click.STRING}, None), # No default specified, should be None. + ({"type": click.BOOL, "default": False}, False), + ({"type": click.BOOL, "default": True}, True), + ({"type": click.FLOAT, "default": 3.14}, 3.14), + # Multiple with default. + ({"multiple": True, "default": [1, 2, 3]}, (1, 2, 3)), + ({"multiple": True, "default": ()}, ()), + # Required option without value should use SENTINEL behavior. + ({"required": False}, None), + # Choice type with default. + ({"type": click.Choice(["a", "b", "c"]), "default": "b"}, "b"), + # Path type with default. + ({"type": click.Path(), "default": "/tmp"}, "/tmp"), + # Flag options. + ({"is_flag": True, "default": False}, False), + ({"is_flag": True, "default": True}, True), + # Count option. + ({"count": True}, 0), + # Hidden option. + ({"hidden": True, "default": "secret"}, "secret"), + ), +) +def test_other_command_invoke_with_defaults(runner, opt_params, expected): @click.command() @click.pass_context def cli(ctx): return ctx.invoke(other_cmd) @click.command() - @click.option("-a", type=click.INT, default=42) - @click.option("-b", type=click.INT, default="15") - @click.option("-c", multiple=True) + @click.option("-a", **opt_params) @click.pass_context - def other_cmd(ctx, a, b, c): - return ctx.info_name, a, b, c + def other_cmd(ctx, a): + return ctx.info_name, a result = runner.invoke(cli, standalone_mode=False) - # invoke should type cast default values, str becomes int, empty - # multiple should be empty tuple instead of None - assert result.return_value == ("other", 42, 15, ()) + + assert result.return_value == ("other", expected) def test_invoked_subcommand(runner): From 12fdbbe6fb8ab136eb0ba62ea80bf5ea52a98322 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Wed, 24 Sep 2025 08:27:07 +0200 Subject: [PATCH 2/2] Format reference to code objects --- CHANGES.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 602aa22c0..95395eb28 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,8 @@ Version 8.3.x Unreleased -- Don't discard pager arguments by correctly using subprocess.Popen. :issue:`3039` :pr:`3055` +- Don't discard pager arguments by correctly using ``subprocess.Popen``. :issue:`3039` + :pr:`3055` - Replace ``Sentinel.UNSET`` default values by ``None`` as they're passed through the ``Context.invoke()`` method. :issue:`3066` :issue:`3065` :pr:`3068` @@ -32,7 +33,7 @@ Released 2025-09-17 - Lazily import ``shutil``. :pr:`3023` - Properly forward exception information to resources registered with ``click.core.Context.with_resource()``. :issue:`2447` :pr:`3058` -- Fix regression related to EOF handling in CliRunner. :issue:`2939` :pr:`2940` +- Fix regression related to EOF handling in ``CliRunner``. :issue:`2939` :pr:`2940` Version 8.2.2 -------------