diff --git a/CHANGES.md b/CHANGES.md index b8bdefb68..7bdf90595 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ Unreleased - A `KeyboardInterrupt` arriving while `Command.main()` reports an abort or an error, or while it exits, no longer escapes as an unhandled traceback. The command still exits with the intended code; only the message may be lost. {issue}`3802` +- Add {meth}`Option.get_help_spec`, which returns the option's left help + column even when the option is hidden. {meth}`Option.get_help_record` still + returns `None` for hidden options, so help screens are unchanged. {pr}`3821` - Document which types are inferred from `default`, and what an unrecognized `type` callable does to a command-line value. {issue}`3036` {pr}`3808` diff --git a/src/click/core.py b/src/click/core.py index c3e68b111..18a43ac5f 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -3363,10 +3363,15 @@ def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: nargs=self.nargs, ) - def get_help_record(self, ctx: Context) -> tuple[str, str] | None: - if self.hidden: - return None + def get_help_spec(self, ctx: Context) -> str: + """Returns the left column of the option's help record: its spellings + and metavar, like ``-v, --verbose`` or ``-c, --config TEXT``. + Unlike :meth:`get_help_record`, the spec is produced even when the + option is :attr:`hidden`. + + .. versionadded:: 8.5.1 + """ any_prefix_is_slash = False def _write_opts(opts: cabc.Sequence[str]) -> str: @@ -3387,6 +3392,12 @@ def _write_opts(opts: cabc.Sequence[str]) -> str: if self.secondary_opts: rv.append(_write_opts(self.secondary_opts)) + return ("; " if any_prefix_is_slash else " / ").join(rv) + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + if self.hidden: + return None + help = self.help or "" extra = self.get_help_extra(ctx) @@ -3406,7 +3417,7 @@ def _write_opts(opts: cabc.Sequence[str]) -> str: extra_str = "; ".join(extra_items) help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" - return ("; " if any_prefix_is_slash else " / ").join(rv), help + return self.get_help_spec(ctx), help def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra: extra: types.OptionHelpExtra = {} diff --git a/tests/test_options.py b/tests/test_options.py index 5438a8c5c..0656a0b54 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -72,6 +72,30 @@ def test_deprecated_empty_help_no_leading_space(help_text, deprecated, expected) assert opt.get_help_record(ctx)[1] == expected +@pytest.mark.parametrize( + ("param_decls", "kwargs", "expected"), + [ + (["-v", "--verbose"], {"is_flag": True}, "-v, --verbose"), + (["--config"], {}, "--config TEXT"), + (["--color/--no-color"], {}, "--color / --no-color"), + (["/debug;/no-debug"], {}, "/debug; /no-debug"), + ], +) +@pytest.mark.parametrize("hidden", [False, True]) +def test_help_spec(param_decls, kwargs, hidden, expected): + """A hidden option still produces its spec via ``get_help_spec()``, unlike + ``get_help_record()``. + """ + opt = click.Option(param_decls, hidden=hidden, **kwargs) + ctx = click.Context(click.Command("cli")) + assert opt.get_help_spec(ctx) == expected + + if hidden: + assert opt.get_help_record(ctx) is None + else: + assert opt.get_help_record(ctx)[0] == expected + + @pytest.mark.parametrize("deprecated", [True, "USE B INSTEAD"]) def test_deprecated_warning(runner, deprecated): @click.command()