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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Version 8.5.0

Unreleased

- :class:`Argument` accepts a ``help`` parameter, and help output includes
an ``Arguments`` section when argument help is available. :issue:`2983`


Version 8.4.1
-------------
Expand Down
5 changes: 4 additions & 1 deletion docs/arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ Arguments are:
* Are positional in nature.
* Similar to a limited version of {ref}`options <options>` that
can take an arbitrary number of inputs
* {ref}`Documented manually <documenting-arguments>`.
* Can take an optional `help` string shown in the ``Arguments`` section of
the help page, or be {ref}`documented in the command docstring
<documenting-arguments>`.

Useful and often used kwargs are:

* `help`: Help text for the argument.
* `default`: Passes a default.
* `nargs`: Sets the number of arguments. Set to -1 to take an arbitrary number.

Expand Down
12 changes: 7 additions & 5 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Simple example:
.. click:example::

@click.command()
@click.argument('name')
@click.argument('name', help='The name to print')
@click.option('--count', default=1, help='number of greetings')
def hello(name: str, count: int):
"""This script prints hello and a name one or more times."""
Expand Down Expand Up @@ -73,16 +73,18 @@ The help epilog is printed at the end of the help and is useful for showing exam

## Documenting Arguments

{class}`click.argument` does not take a `help` parameter. This follows the Unix Command Line Tools convention of using arguments only for necessary things and documenting them in the command help text
by name. This should then be done via the docstring.
{class}`click.argument` accepts an optional `help` parameter that is shown in
the ``Arguments`` section of the help page. You can still document arguments
in the command docstring, especially when you want to describe them in the
main help text by name.

A brief example:

```{eval-rst}
.. click:example::

@click.command()
@click.argument('filename')
@click.argument('filename', help='The file to print.')
def touch(filename):
"""Print FILENAME."""
click.echo(filename)
Expand All @@ -91,7 +93,7 @@ A brief example:
invoke(touch, args=['--help'])
```

Or more explicitly:
Or more explicitly in the docstring:

```{eval-rst}
.. click:example::
Expand Down
38 changes: 37 additions & 1 deletion src/click/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,11 +1199,19 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:

def format_options(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes all the options into the formatter if they exist."""
args = []
opts = []
for param in self.get_params(ctx):
rv = param.get_help_record(ctx)
if rv is not None:
opts.append(rv)
if isinstance(param, Argument):
args.append(rv)
else:
opts.append(rv)

if args:
with formatter.section(_("Arguments")):
formatter.write_dl(args)

if opts:
with formatter.section(_("Options")):
Expand Down Expand Up @@ -3435,6 +3443,11 @@ class Argument(Parameter):
and are required by default.

All parameters are passed onwards to the constructor of :class:`Parameter`.

:param help: the help string.

.. versionchanged:: 8.5
Added the ``help`` parameter.
"""

param_type_name = "argument"
Expand All @@ -3443,6 +3456,7 @@ def __init__(
self,
param_decls: cabc.Sequence[str],
required: bool | None = None,
help: str | None = None,
**attrs: t.Any,
) -> None:
# Auto-detect the requirement status of the argument if not explicitly set.
Expand All @@ -3458,8 +3472,24 @@ def __init__(
if "multiple" in attrs:
raise TypeError("__init__() got an unexpected keyword argument 'multiple'.")

deprecated = attrs.get("deprecated", False)

if help:
help = inspect.cleandoc(help)

if deprecated:
label = _format_deprecated_label(deprecated)
help = f"{help} {label}" if help is not None else label

self.help = help

super().__init__(param_decls, required=required, **attrs)

def to_info_dict(self) -> dict[str, t.Any]:
info_dict = super().to_info_dict()
info_dict.update(help=self.help)
return info_dict

@property
def human_readable_name(self) -> str:
if self.metavar is not None:
Expand Down Expand Up @@ -3502,6 +3532,12 @@ def _parse_decls(
def get_usage_pieces(self, ctx: Context) -> list[str]:
return [self.make_metavar(ctx)]

def get_help_record(self, ctx: Context) -> tuple[str, str] | None:
if self.help is None:
return None

return self.make_metavar(ctx), self.help

def get_error_hint(self, ctx: Context | None) -> str:
if ctx is not None:
return f"'{self.make_metavar(ctx)}'"
Expand Down
42 changes: 42 additions & 0 deletions tests/test_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,48 @@ def cli(f):
assert result.output == "test\n"


def test_argument_help(runner):
@click.command()
@click.argument("name", help="The name to print")
@click.option("--count", default=1, help="number of greetings")
def cli(name, count):
pass

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "Arguments:" in result.output
assert "NAME" in result.output
assert "The name to print" in result.output
assert "Options:" in result.output
assert "number of greetings" in result.output
assert result.output.index("Arguments:") < result.output.index("Options:")


def test_argument_help_options_only_no_arguments_section(runner):
@click.command()
@click.option("--count", default=1, help="number of greetings")
def cli(count):
pass

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "Arguments:" not in result.output
assert "Options:" in result.output
assert "number of greetings" in result.output


def test_argument_help_optional_metavar(runner):
@click.command()
@click.argument("name", required=False, default="", help="The name to print")
def cli(name):
pass

result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0, result.output
assert "[NAME]" in result.output
assert "The name to print" in result.output


def test_deprecated_usage(runner):
@click.command()
@click.argument("f", required=False, deprecated=True)
Expand Down
1 change: 1 addition & 0 deletions tests/test_info_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"multiple": False,
"default": None,
"envvar": None,
"help": None,
},
)
NUMBER_OPTION = (
Expand Down
Loading