Skip to content

Commit 047adef

Browse files
kdeldyckejspricke
andcommitted
Preserve declaration order of help option names
Co-authored-by: Jochen Sprickerhof <git@jochen.sprickerhof.de>
1 parent 00e592c commit 047adef

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Unreleased
6363
{class}`ParamType` takes a second optional type parameter describing the
6464
input value it accepts (`ParamType[int, str]` for a type converting
6565
strings to integers), defaulting to `Any`. {pr}`3407`
66+
- {meth}`Command.get_help_option_names` returns the help option names in the
67+
order they were declared. {pr}`3728`
6668

6769
## Version 8.4.2
6870

src/click/core.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,11 +1175,18 @@ def collect_usage_pieces(self, ctx: Context) -> list[str]:
11751175
return rv
11761176

11771177
def get_help_option_names(self, ctx: Context) -> list[str]:
1178-
"""Returns the names for the help option."""
1179-
all_names = set(ctx.help_option_names)
1178+
"""Returns the names for the help option.
1179+
1180+
Drops duplicates and names already reserved by another parameter. Order of
1181+
:attr:`Context.help_option_names` is preserved, so the result is stable.
1182+
1183+
.. versionchanged:: 8.5.0
1184+
Names keep their declaration order.
1185+
"""
1186+
all_names = dict.fromkeys(ctx.help_option_names)
11801187
for param in self.params:
1181-
all_names.difference_update(param.opts)
1182-
all_names.difference_update(param.secondary_opts)
1188+
for name in (*param.opts, *param.secondary_opts):
1189+
all_names.pop(name, None)
11831190
return list(all_names)
11841191

11851192
def get_help_option(self, ctx: Context) -> Option | None:

tests/test_commands.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,56 @@ def test_iter_params_for_processing(
419419
)
420420

421421

422+
@pytest.mark.parametrize(
423+
("help_option_names", "params", "expected"),
424+
[
425+
# A single default name is returned unchanged.
426+
(["--help"], [], ["--help"]),
427+
# Declaration order is preserved verbatim, whatever it is.
428+
(["-h", "--help"], [], ["-h", "--help"]),
429+
(["--help", "-h"], [], ["--help", "-h"]),
430+
(["--help", "-h", "-?"], [], ["--help", "-h", "-?"]),
431+
(["-?", "--help", "-h"], [], ["-?", "--help", "-h"]),
432+
# Duplicate names collapse to their first occurrence, keeping order.
433+
(["--help", "--help"], [], ["--help"]),
434+
(["-h", "--help", "-h"], [], ["-h", "--help"]),
435+
# A name already claimed by another parameter's option is dropped.
436+
(["-h", "--help"], [["--help"]], ["-h"]),
437+
(["-h", "--help"], [["-h", "--verbose"]], ["--help"]),
438+
# Both options of a feature-switch flag are removed too.
439+
(["--shout", "--help"], [["--shout/--no-shout"]], ["--help"]),
440+
(["--no-shout", "--help"], [["--shout/--no-shout"]], ["--help"]),
441+
# Filtering every name out yields an empty list.
442+
(["-h", "--help"], [["-h"], ["--help"]], []),
443+
# Deduplication and conflict removal combine, order still preserved.
444+
(["-h", "--help", "-h", "--assist"], [["--assist"]], ["-h", "--help"]),
445+
],
446+
ids=[
447+
"default",
448+
"order-short-long",
449+
"order-long-short",
450+
"order-three",
451+
"order-three-shuffled",
452+
"dedupe-adjacent",
453+
"dedupe-spread",
454+
"conflict-long",
455+
"conflict-short",
456+
"conflict-flag-on",
457+
"conflict-flag-off",
458+
"all-removed",
459+
"dedupe-and-conflict",
460+
],
461+
)
462+
def test_get_help_option_names(help_option_names, params, expected):
463+
"""Check help option names are deduplicated but keep stable order.
464+
465+
https://github.com/pallets/click/pull/3728
466+
"""
467+
cli = click.Command("cli", params=[click.Option(decls) for decls in params])
468+
ctx = click.Context(cli, help_option_names=help_option_names)
469+
assert cli.get_help_option_names(ctx) == expected
470+
471+
422472
def test_help_param_priority(runner):
423473
"""Cover the edge-case in which the eagerness of help option was not
424474
respected, because it was internally generated multiple times.

0 commit comments

Comments
 (0)