Skip to content

Commit 3bb230d

Browse files
authored
WIP: Fix HelpFormatter.write_usage producing spurious characters (#3434)
2 parents 63274a7 + 0551bf5 commit 3bb230d

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ Unreleased
5959
- :class:`~click.formatting.TextWrapper` and
6060
:func:`~click.formatting.wrap_text` now measure line width in visible
6161
characters, ignoring ANSI escape sequences. :pr:`3420`
62+
- Fix :meth:`HelpFormatter.write_usage` emitting only a blank line when
63+
called without ``args``. The usage prefix and program name are now
64+
written even when no arguments follow, and the trailing separator
65+
space is stripped so the line ends at the program name.
66+
:issue:`3360` :pr:`3434`
6267
- Show custom error messages from types when :func:`prompt` with
6368
``hide_input=True`` fails validation, instead of always showing a
6469
generic message. Built-in type messages mask the input value.

src/click/formatting.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N
164164
usage_prefix = f"{prefix:>{self.current_indent}}{prog} "
165165
text_width = self.width - self.current_indent
166166

167+
if not args:
168+
# Without args, the prefix's trailing space and the wrap_text
169+
# call that would normally place args on the line are both
170+
# unnecessary. Emit just the prefix line.
171+
self.write(usage_prefix.rstrip(" "))
172+
self.write("\n")
173+
return
174+
167175
if text_width >= (term_len(usage_prefix) + 20):
168176
# The arguments will fit to the right of the prefix.
169177
indent = " " * term_len(usage_prefix)

tests/test_formatting.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,118 @@ def test_write_usage_styled_prefix_keeps_options_on_one_line():
499499

500500
visible = strip_ansi(rendered)
501501
assert visible == "Usage: cli [OPTIONS]\n"
502+
503+
504+
@pytest.mark.parametrize(
505+
("formatter_kwargs", "current_indent", "prog", "args", "prefix", "expected"),
506+
[
507+
# Issue #3360: the default prefix used to emit only
508+
# a blank line because ``wrap_text("", initial_indent=usage_prefix)``
509+
# returned ``""`` and discarded the prefix.
510+
pytest.param(
511+
{},
512+
0,
513+
"Program",
514+
"",
515+
None,
516+
"Usage: Program\n",
517+
id="empty-args-default-prefix",
518+
),
519+
# A caller-supplied prefix is preserved verbatim.
520+
pytest.param(
521+
{},
522+
0,
523+
"Program",
524+
"",
525+
"Run: ",
526+
"Run: Program\n",
527+
id="empty-args-custom-prefix",
528+
),
529+
# ``current_indent`` is preserved even with no args to render.
530+
pytest.param(
531+
{},
532+
4,
533+
"Program",
534+
"",
535+
None,
536+
"Usage: Program\n",
537+
id="empty-args-indented",
538+
),
539+
# Prog too long to share a line with args: the wrap branch must not
540+
# emit a second line.
541+
pytest.param(
542+
{"width": 20},
543+
0,
544+
"VeryLongProgramName",
545+
"",
546+
None,
547+
"Usage: VeryLongProgramName\n",
548+
id="empty-args-long-prog",
549+
),
550+
# With non-empty args, the separator space between prog and args is preserved.
551+
pytest.param(
552+
{},
553+
0,
554+
"Program",
555+
"[OPTIONS]",
556+
None,
557+
"Usage: Program [OPTIONS]\n",
558+
id="with-args-default-prefix",
559+
),
560+
],
561+
)
562+
def test_help_formatter_write_usage(
563+
formatter_kwargs, current_indent, prog, args, prefix, expected
564+
):
565+
"""``HelpFormatter.write_usage`` renders a single usage line whose
566+
trailing separator tracks whether ``args`` is non-empty.
567+
"""
568+
f = click.HelpFormatter(**formatter_kwargs)
569+
f.current_indent = current_indent
570+
if prefix is None:
571+
f.write_usage(prog, args)
572+
else:
573+
f.write_usage(prog, args, prefix=prefix)
574+
assert f.getvalue() == expected
575+
576+
577+
def test_help_formatter_write_usage_without_args_styled_prefix():
578+
"""A downstream-styled prefix is preserved when ``args`` is empty:
579+
the ANSI escape sequences survive, only the trailing separator is
580+
removed.
581+
"""
582+
styled_prefix = "\x1b[38;2;38;139;210m\x1b[1mUsage:\x1b[0m "
583+
f = click.HelpFormatter()
584+
f.write_usage("cli", prefix=styled_prefix)
585+
rendered = f.getvalue()
586+
assert strip_ansi(rendered) == "Usage: cli\n"
587+
assert "\x1b[" in rendered
588+
589+
590+
@pytest.mark.parametrize(
591+
("command_kwargs", "expected_usage_line"),
592+
[
593+
# End-to-end regression for #3360: an empty ``options_metavar`` with
594+
# no parameters used to render a blank usage line.
595+
pytest.param(
596+
{"options_metavar": ""},
597+
"Usage: cli",
598+
id="empty-options-metavar-no-params",
599+
),
600+
# End-to-end regression: ``options_metavar=None`` is the documented
601+
# way to suppress the ``[OPTIONS]`` slot entirely.
602+
pytest.param(
603+
{"options_metavar": None},
604+
"Usage: cli",
605+
id="none-options-metavar-no-params",
606+
),
607+
],
608+
)
609+
def test_command_write_usage_no_args(runner, command_kwargs, expected_usage_line):
610+
"""End-to-end: a command with no parameters and an empty or absent
611+
``options_metavar`` renders a usage line with just the program name,
612+
no trailing space.
613+
"""
614+
cli = click.Command("cli", **command_kwargs)
615+
result = runner.invoke(cli, ["--help"])
616+
assert result.output.splitlines()[0] == expected_usage_line

0 commit comments

Comments
 (0)