Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Unreleased
{class}`ParamType` takes a second optional type parameter describing the
input value it accepts (`ParamType[int, str]` for a type converting
strings to integers), defaulting to `Any`. {pr}`3407`
- {meth}`Command.get_help_option_names` returns the help option names in the
order they were declared. {pr}`3728`

## Version 8.4.2

Expand Down
15 changes: 11 additions & 4 deletions src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,11 +1175,18 @@ def collect_usage_pieces(self, ctx: Context) -> list[str]:
return rv

def get_help_option_names(self, ctx: Context) -> list[str]:
"""Returns the names for the help option."""
all_names = set(ctx.help_option_names)
"""Returns the names for the help option.

Drops duplicates and names already reserved by another parameter. Order of
:attr:`Context.help_option_names` is preserved, so the result is stable.

.. versionchanged:: 8.5.0
Names keep their declaration order.
"""
all_names = dict.fromkeys(ctx.help_option_names)
for param in self.params:
all_names.difference_update(param.opts)
all_names.difference_update(param.secondary_opts)
for name in (*param.opts, *param.secondary_opts):
all_names.pop(name, None)
return list(all_names)

def get_help_option(self, ctx: Context) -> Option | None:
Expand Down
50 changes: 50 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,56 @@ def test_iter_params_for_processing(
)


@pytest.mark.parametrize(
("help_option_names", "params", "expected"),
[
# A single default name is returned unchanged.
(["--help"], [], ["--help"]),
# Declaration order is preserved verbatim, whatever it is.
(["-h", "--help"], [], ["-h", "--help"]),
(["--help", "-h"], [], ["--help", "-h"]),
(["--help", "-h", "-?"], [], ["--help", "-h", "-?"]),
(["-?", "--help", "-h"], [], ["-?", "--help", "-h"]),
# Duplicate names collapse to their first occurrence, keeping order.
(["--help", "--help"], [], ["--help"]),
(["-h", "--help", "-h"], [], ["-h", "--help"]),
# A name already claimed by another parameter's option is dropped.
(["-h", "--help"], [["--help"]], ["-h"]),
(["-h", "--help"], [["-h", "--verbose"]], ["--help"]),
# Both options of a feature-switch flag are removed too.
(["--shout", "--help"], [["--shout/--no-shout"]], ["--help"]),
(["--no-shout", "--help"], [["--shout/--no-shout"]], ["--help"]),
# Filtering every name out yields an empty list.
(["-h", "--help"], [["-h"], ["--help"]], []),
# Deduplication and conflict removal combine, order still preserved.
(["-h", "--help", "-h", "--assist"], [["--assist"]], ["-h", "--help"]),
],
ids=[
"default",
"order-short-long",
"order-long-short",
"order-three",
"order-three-shuffled",
"dedupe-adjacent",
"dedupe-spread",
"conflict-long",
"conflict-short",
"conflict-flag-on",
"conflict-flag-off",
"all-removed",
"dedupe-and-conflict",
],
)
def test_get_help_option_names(help_option_names, params, expected):
"""Check help option names are deduplicated but keep stable order.

https://github.com/pallets/click/pull/3728
"""
cli = click.Command("cli", params=[click.Option(decls) for decls in params])
ctx = click.Context(cli, help_option_names=help_option_names)
assert cli.get_help_option_names(ctx) == expected


def test_help_param_priority(runner):
"""Cover the edge-case in which the eagerness of help option was not
respected, because it was internally generated multiple times.
Expand Down