From 71240f8c7f89bfe934b5d0b9f44d79bbf001147f Mon Sep 17 00:00:00 2001 From: Yuri Shevtsov Date: Mon, 18 May 2026 21:48:00 -0400 Subject: [PATCH] Add help parameter to arguments Refactor: pull out args formatter into a dedicated method --- CHANGES.rst | 3 +++ docs/arguments.md | 5 ++++- docs/documentation.md | 12 +++++++----- src/click/core.py | 43 ++++++++++++++++++++++++++++++++++++++++- tests/test_arguments.py | 42 ++++++++++++++++++++++++++++++++++++++++ tests/test_info_dict.py | 1 + 6 files changed, 99 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7488c0c2fc..0cbff88861 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------------- diff --git a/docs/arguments.md b/docs/arguments.md index 90d37c1569..4e8a1d557d 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -10,10 +10,13 @@ Arguments are: * Are positional in nature. * Similar to a limited version of {ref}`options ` that can take an arbitrary number of inputs -* {ref}`Documented manually `. +* Can take an optional `help` string shown in the ``Arguments`` section of + the help page, or be {ref}`documented in the command docstring + `. 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. diff --git a/docs/documentation.md b/docs/documentation.md index a031773344..04c87cbe35 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -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.""" @@ -73,8 +73,10 @@ 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: @@ -82,7 +84,7 @@ A brief example: .. click:example:: @click.command() - @click.argument('filename') + @click.argument('filename', help='The file to print.') def touch(filename): """Print FILENAME.""" click.echo(filename) @@ -91,7 +93,7 @@ A brief example: invoke(touch, args=['--help']) ``` -Or more explicitly: +Or more explicitly in the docstring: ```{eval-rst} .. click:example:: diff --git a/src/click/core.py b/src/click/core.py index 3544c03b3b..899139073b 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1177,6 +1177,7 @@ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: """ self.format_usage(ctx, formatter) self.format_help_text(ctx, formatter) + self.format_args(ctx, formatter) self.format_options(ctx, formatter) self.format_epilog(ctx, formatter) @@ -1202,13 +1203,25 @@ def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: opts = [] for param in self.get_params(ctx): rv = param.get_help_record(ctx) - if rv is not None: + if rv is not None and not isinstance(param, Argument): opts.append(rv) if opts: with formatter.section(_("Options")): formatter.write_dl(opts) + def format_args(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the arguments into the formatter if they exist.""" + args = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None and isinstance(param, Argument): + args.append(rv) + + if args: + with formatter.section(_("Arguments")): + formatter.write_dl(args) + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: """Writes the epilog into the formatter if it exists.""" if self.epilog: @@ -3435,6 +3448,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" @@ -3443,6 +3461,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. @@ -3458,8 +3477,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: @@ -3502,6 +3537,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)}'" diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 2ecf518af5..7dce755fa5 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -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) diff --git a/tests/test_info_dict.py b/tests/test_info_dict.py index 20fe68cc13..f5c0c36858 100644 --- a/tests/test_info_dict.py +++ b/tests/test_info_dict.py @@ -40,6 +40,7 @@ "multiple": False, "default": None, "envvar": None, + "help": None, }, ) NUMBER_OPTION = (