From c53d3732b98c764932e146c007146ad402acb229 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Tue, 6 Oct 2026 15:20:26 +0200 Subject: [PATCH] Fix `show_envvar` output for a sequence or an empty `envvar` --- CHANGES.md | 5 +++++ src/click/core.py | 23 +++++++++++++---------- tests/test_options.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7f1b48543..6875a50be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,11 @@ Unreleased - A command's short help no longer stops at an abbreviation such as `vs.` or `e.g.` inside the first sentence. A period ends the sentence only when it closes the text or the next word does not start in lowercase. {pr}`3865` +- With `show_envvar=True`, an option's error message now prints all variables of an + `envvar` sequence instead of the `repr` of the sequence. {pr}`3884` +- An empty `envvar`, such as `[]` or `""`, shows the same help and error message as + no `envvar` when `show_envvar=True`. The help screen used to show `[env var: ]`. + {pr}`3884` ## Version 8.5.0 diff --git a/src/click/core.py b/src/click/core.py index 9cf05a9b4..cf6f5fca9 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -3270,8 +3270,12 @@ def get_default( def get_error_hint(self, ctx: Context | None) -> str: result = super().get_error_hint(ctx) - if self.show_envvar and self.envvar is not None: - result += f" (env var: '{self.envvar}')" + if self.show_envvar and self.envvar: + envvars = [self.envvar] if isinstance(self.envvar, str) else self.envvar + # An environment variable is allowed to feature a space or a comma on all + # platforms, hence the quotes around each. + var_str = ", ".join(f"'{var}'" for var in envvars) + result += f" (env var: {var_str})" return result def _parse_decls( @@ -3439,15 +3443,14 @@ def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra: if self.show_envvar: envvar = self.envvar - if envvar is None: - if ( - self.allow_from_autoenv - and ctx.auto_envvar_prefix is not None - and self.name - ): - envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + if not envvar and ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" - if envvar is not None: + if envvar: if isinstance(envvar, str): extra["envvars"] = (envvar,) else: diff --git a/tests/test_options.py b/tests/test_options.py index 0656a0b54..b35277fcb 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -875,6 +875,23 @@ def foo_bar(baz): assert "TEST_FOO_BAR_BAZ" in result.output +@pytest.mark.parametrize("envvar", [None, [], ""]) +@pytest.mark.parametrize( + ("auto_envvar_prefix", "expected"), + [ + (None, " --units TEXT\n"), + ("WEATHER", " --units TEXT [env var: WEATHER_UNITS]\n"), + ], +) +def test_show_envvar_empty(runner, envvar, auto_envvar_prefix, expected): + """An empty ``envvar`` shows the same help as no ``envvar``.""" + option = click.Option(["--units"], envvar=envvar, show_envvar=True) + cli = click.Command("cli", params=[option]) + result = runner.invoke(cli, ["--help"], auto_envvar_prefix=auto_envvar_prefix) + assert not result.exception + assert expected in result.output + + def test_custom_validation(runner): def validate_pos_int(ctx, param, value): if value < 0: @@ -1018,6 +1035,25 @@ def test_missing_envvar(runner): assert "Error: Missing option '--foo'." in result.output +@pytest.mark.parametrize( + ("envvar", "expected"), + [ + ("UNITS", "'--units' (env var: 'UNITS')"), + (["UNITS"], "'--units' (env var: 'UNITS')"), + (["WEATHER_UNITS", "UNITS"], "'--units' (env var: 'WEATHER_UNITS', 'UNITS')"), + (("WEATHER_UNITS", "UNITS"), "'--units' (env var: 'WEATHER_UNITS', 'UNITS')"), + ([], "'--units'"), + ("", "'--units'"), + ], +) +def test_missing_envvar_sequence(runner, envvar, expected): + """The error hint names each variable of a sequence ``envvar``.""" + option = click.Option(["--units"], envvar=envvar, show_envvar=True, required=True) + result = runner.invoke(click.Command("cli", params=[option])) + assert result.exit_code == 2 + assert f"Error: Missing option {expected}." in result.output + + def test_case_insensitive_choice(runner): @click.command() @click.option("--foo", type=click.Choice(["Orange", "Apple"], case_sensitive=False))