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
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Unreleased
{func}`get_pager_file` picked for the output stream, and with
`errors="replace"` to match the pipe backend. Any text stdout can encode
reaches the pager.
- {meth}`HelpFormatter.write_usage` no longer breaks an argument at a hyphen it
contains, so a long option name or metavar reaching the wrap width moves to
the next line whole instead of being split across two. {func}`wrap_text`
takes a `break_on_hyphens` argument to control this. {issue}`3362`

## Version 8.4.2

Expand Down
20 changes: 19 additions & 1 deletion src/click/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def wrap_text(
initial_indent: str = "",
subsequent_indent: str = "",
preserve_paragraphs: bool = False,
break_on_hyphens: bool = True,
) -> str:
"""A helper function that intelligently wraps text. By default, it
assumes that it operates on a single paragraph of text but if the
Expand All @@ -52,6 +53,13 @@ def wrap_text(
each consecutive line.
:param preserve_paragraphs: if this flag is set then the wrapping will
intelligently handle paragraphs.
:param break_on_hyphens: whether a word may be broken after a hyphen it
contains. Set this to ``False`` for text made of
hyphenated tokens, such as option names, that
should stay on one line.

.. versionchanged:: 8.5.0
Added the ``break_on_hyphens`` parameter.

.. versionchanged:: 8.4.0
Width is measured in visible characters. ANSI escape sequences in
Expand All @@ -67,6 +75,7 @@ def wrap_text(
initial_indent=initial_indent,
subsequent_indent=subsequent_indent,
replace_whitespace=False,
break_on_hyphens=break_on_hyphens,
)
if not preserve_paragraphs:
return wrapper.fill(text)
Expand Down Expand Up @@ -162,6 +171,10 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N
:param args: whitespace separated list of arguments.
:param prefix: The prefix for the first line. Defaults to
``"Usage: "``.

.. versionchanged:: 8.5.0
Wrapping no longer breaks an argument at a hyphen it contains, so
option names and metavars stay on one line.
"""
if prefix is None:
prefix = "{usage} ".format(usage=_("Usage:"))
Expand All @@ -186,6 +199,7 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N
text_width,
initial_indent=usage_prefix,
subsequent_indent=indent,
break_on_hyphens=False,
)
)
else:
Expand All @@ -195,7 +209,11 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N
indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
self.write(
wrap_text(
args, text_width, initial_indent=indent, subsequent_indent=indent
args,
text_width,
initial_indent=indent,
subsequent_indent=indent,
break_on_hyphens=False,
)
)

Expand Down
53 changes: 53 additions & 0 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,56 @@ def test_command_write_usage_no_args(runner, command_kwargs, expected_usage_line
cli = click.Command("cli", **command_kwargs)
result = runner.invoke(cli, ["--help"])
assert result.output.splitlines()[0] == expected_usage_line


def test_wrap_text_break_on_hyphens():
"""``wrap_text`` breaks a hyphenated word by default, and keeps it whole
when ``break_on_hyphens`` is disabled.
"""
text = "alpha --max-retry-count"
assert click.formatting.wrap_text(text, width=20) == "alpha --max-retry-\ncount"
assert (
click.formatting.wrap_text(text, width=20, break_on_hyphens=False)
== "alpha\n--max-retry-count"
)


def test_write_usage_keeps_hyphenated_args_whole():
"""Issue #3362: an argument reaching the wrap width used to be split at
an internal hyphen, so an option name ended up spread over two lines.
"""
options = [
"--enable-verbose-logging",
"--output-file-path",
"--max-retry-count",
"--disable-cache-mode",
"--config-file-location",
"--user-auth-token",
"--auto-update-interval",
"--force-overwrite-existing",
"--network-timeout-seconds",
"--debug-trace-enabled",
]
f = click.HelpFormatter(width=65)
f.write_usage("program", " ".join(options))
lines = f.getvalue().splitlines()

for option in options:
assert any(option in line for line in lines)

for line in lines:
assert not line.endswith("-")


def test_write_usage_keeps_hyphenated_args_whole_below_prefix():
"""Issue #3362: the branch that puts the arguments on their own line,
used when the prefix is too long to share one, wraps the same way.
"""
f = click.HelpFormatter(width=31)
f.write_usage("a-program-with-a-very-long-name", "[OPTIONS] --max-retry-count")
lines = f.getvalue().splitlines()

assert any("--max-retry-count" in line for line in lines)

for line in lines:
assert not line.endswith("-")