Skip to content

Commit 775a1cf

Browse files
committed
Fix parsing when a parameter is named help
Fixes #2819
1 parent 7925a34 commit 775a1cf

4 files changed

Lines changed: 132 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Unreleased
1515
produced by a callback, covering cases {func}`version_option` intentionally
1616
does not. The feature set of {func}`version_option` is now frozen; see
1717
[discussion #3527](https://github.com/pallets/click/discussions/3527). {pr}`3581`
18+
- The automatic help option stores its value under a name no other parameter
19+
uses. A parameter named `help`, like `click.argument("help")`, no longer
20+
makes both unusable. {issue}`2819` {pr}`3678`
1821

1922
## Version 8.4.2
2023

docs/documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ The default placeholder variable ([meta variable](https://en.wikipedia.org/wiki/
322322

323323
## Help Parameter Customization
324324

325-
Help parameters are automatically added by Click for any command. The default is `--help` but can be overridden by the context setting {attr}`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter, so if a command itself implements a parameter named `help` then the default help will not be run.
325+
Help parameters are automatically added by Click for any command. The default is `--help` but can be overridden by the context setting {attr}`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter: if the command itself implements an option reusing one of the help option names, the default help parameter stops accepting that name, and is dropped entirely once all its names are taken. A parameter that merely stores its value under the name `help`, like `click.argument("help")`, does not disable the default help option: both keep working independently.
326326

327327
This example changes the default parameters to `-h` and `--help`
328328
instead of just `--help`:

src/click/core.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,10 @@ def get_help_option(self, ctx: Context) -> Option | None:
11531153
11541154
Skipped if :attr:`add_help_option` is ``False``.
11551155
1156+
.. versionchanged:: 8.5.0
1157+
The help option is stored under a name no other parameter
1158+
uses, so a parameter named ``help`` no longer breaks parsing.
1159+
11561160
.. versionchanged:: 8.1.8
11571161
The help option is now cached to avoid creating it multiple times.
11581162
"""
@@ -1169,9 +1173,20 @@ def get_help_option(self, ctx: Context) -> Option | None:
11691173
# Avoid circular import.
11701174
from .decorators import help_option
11711175

1172-
# Apply help_option decorator and pop resulting option
1176+
# Apply help_option decorator and pop resulting option.
11731177
help_option(*help_option_names)(self)
1174-
self._help_option = self.params.pop() # type: ignore[assignment]
1178+
option = self.params.pop()
1179+
1180+
# The parser stores parsed values keyed by parameter name, so
1181+
# sharing the name of another parameter (like an argument named
1182+
# "help") would make both clobber each other's value. The help
1183+
# option never exposes its value, so it is free to deviate from
1184+
# the name derived from its flags.
1185+
taken_names = {param.name for param in self.params}
1186+
while option.name in taken_names:
1187+
option.name = f"{option.name}_"
1188+
1189+
self._help_option = option # type: ignore[assignment]
11751190

11761191
return self._help_option
11771192

tests/test_basic.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,117 @@ def cli():
2828
assert result.exit_code == 0
2929

3030

31+
@pytest.mark.parametrize(
32+
("help_names", "params", "args", "expected_output", "expected_name"),
33+
[
34+
(["--help"], [], [], "\n", "help"),
35+
(["--help"], [click.Argument(["help"])], ["value"], "value\n", "help_"),
36+
(
37+
["--help"],
38+
[click.Option(["--assist", "help"])],
39+
["--assist", "value"],
40+
"value\n",
41+
"help_",
42+
),
43+
(
44+
["--help"],
45+
[click.Argument(["help"]), click.Option(["--other", "help_"])],
46+
["value"],
47+
"value\n",
48+
"help__",
49+
),
50+
(
51+
["--man"],
52+
[click.Option(["--foo", "man"])],
53+
["--foo", "value"],
54+
"value\n",
55+
"man_",
56+
),
57+
(
58+
["--man"],
59+
[click.Option(["--foo", "man"]), click.Argument(["man"])],
60+
["value"],
61+
"value\n",
62+
"man_",
63+
),
64+
(
65+
["--man"],
66+
[click.Option(["--foo", "man"]), click.Option(["--bar", "man_"])],
67+
["--foo", "value"],
68+
"value\n",
69+
"man__",
70+
),
71+
(
72+
["--man"],
73+
[click.Option(["--bar", "man_"])],
74+
["--bar", "value"],
75+
"value\n",
76+
"man",
77+
),
78+
],
79+
ids=[
80+
"no-collision",
81+
"argument",
82+
"option",
83+
"chained-collisions",
84+
"derived-name",
85+
"duplicate-names",
86+
"derived-chained-collisions",
87+
"derived-name-free",
88+
],
89+
)
90+
def test_param_named_help(
91+
runner, help_names, params, args, expected_output, expected_name
92+
):
93+
"""A parameter storing its value under the same name as the automatic
94+
help option does not clash with it.
95+
96+
https://github.com/pallets/click/issues/2819
97+
"""
98+
cli = click.Command(
99+
"cli",
100+
context_settings={"help_option_names": help_names},
101+
params=params,
102+
callback=lambda **kwargs: click.echo(next(iter(kwargs.values()), None)),
103+
)
104+
105+
ctx = click.Context(cli, help_option_names=help_names)
106+
help_option = cli.get_help_option(ctx)
107+
assert help_option is not None
108+
assert help_option.name == expected_name
109+
110+
result = runner.invoke(cli, args)
111+
assert result.output == expected_output
112+
assert result.exit_code == 0
113+
114+
result = runner.invoke(cli, [help_names[0]])
115+
assert "Show this message and exit." in result.output
116+
assert result.exit_code == 0
117+
118+
119+
def test_option_reusing_help_flag(runner):
120+
"""An option reusing the ``--help`` flag replaces the automatic help
121+
option entirely.
122+
123+
https://github.com/pallets/click/issues/2819
124+
"""
125+
126+
@click.command()
127+
@click.option("--help", default="default value")
128+
def cli(help):
129+
click.echo(help)
130+
131+
assert cli.get_help_option(click.Context(cli)) is None
132+
133+
result = runner.invoke(cli, [])
134+
assert result.output == "default value\n"
135+
assert result.exit_code == 0
136+
137+
result = runner.invoke(cli, ["--help", "custom"])
138+
assert result.output == "custom\n"
139+
assert result.exit_code == 0
140+
141+
31142
def test_repr():
32143
@click.command()
33144
def command():

0 commit comments

Comments
 (0)