Skip to content

Commit bb8244c

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

4 files changed

Lines changed: 86 additions & 2 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: 14 additions & 1 deletion
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,8 +1173,17 @@ def get_help_option(self, ctx: Context) -> Option | None:
11691173
# Avoid circular import.
11701174
from .decorators import help_option
11711175

1176+
# The parser stores parsed values keyed by parameter name, so
1177+
# sharing the name of another parameter (like an argument named
1178+
# "help") would make both clobber each other's value. The help
1179+
# option never exposes its value, so any free name works.
1180+
taken_names = {param.name for param in self.params}
1181+
storage_name = "help"
1182+
while storage_name in taken_names:
1183+
storage_name += "_"
1184+
11721185
# Apply help_option decorator and pop resulting option
1173-
help_option(*help_option_names)(self)
1186+
help_option(*help_option_names, storage_name)(self)
11741187
self._help_option = self.params.pop() # type: ignore[assignment]
11751188

11761189
return self._help_option

tests/test_basic.py

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

3030

31+
@pytest.mark.parametrize(
32+
("params", "args", "expected_output", "expected_name"),
33+
[
34+
([], [], "\n", "help"),
35+
([click.Argument(["help"])], ["value"], "value\n", "help_"),
36+
(
37+
[click.Option(["--assist", "help"])],
38+
["--assist", "value"],
39+
"value\n",
40+
"help_",
41+
),
42+
(
43+
[click.Argument(["help"]), click.Option(["--other", "help_"])],
44+
["value"],
45+
"value\n",
46+
"help__",
47+
),
48+
],
49+
ids=["no-collision", "argument", "option", "chained-collisions"],
50+
)
51+
def test_param_named_help(runner, params, args, expected_output, expected_name):
52+
"""A parameter storing its value under the name ``help`` does not clash
53+
with the automatic help option: the latter keeps its default ``help``
54+
storage name unless a user parameter claims it, then underscores are
55+
appended until the name is free.
56+
57+
https://github.com/pallets/click/issues/2819
58+
"""
59+
cli = click.Command(
60+
"cli", params=params, callback=lambda **kwargs: click.echo(kwargs.get("help"))
61+
)
62+
63+
help_option = cli.get_help_option(click.Context(cli))
64+
assert help_option is not None
65+
assert help_option.name == expected_name
66+
67+
result = runner.invoke(cli, args)
68+
assert result.output == expected_output
69+
assert result.exit_code == 0
70+
71+
result = runner.invoke(cli, ["--help"])
72+
assert "Show this message and exit." in result.output
73+
assert result.exit_code == 0
74+
75+
76+
def test_option_reusing_help_flag(runner):
77+
"""An option reusing the ``--help`` flag replaces the automatic help
78+
option entirely.
79+
80+
https://github.com/pallets/click/issues/2819
81+
"""
82+
83+
@click.command()
84+
@click.option("--help", default="default value")
85+
def cli(help):
86+
click.echo(help)
87+
88+
assert cli.get_help_option(click.Context(cli)) is None
89+
90+
result = runner.invoke(cli, [])
91+
assert result.output == "default value\n"
92+
assert result.exit_code == 0
93+
94+
result = runner.invoke(cli, ["--help", "custom"])
95+
assert result.output == "custom\n"
96+
assert result.exit_code == 0
97+
98+
3199
def test_repr():
32100
@click.command()
33101
def command():

0 commit comments

Comments
 (0)