Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Version 8.4.0

Unreleased

- Don't split hyphenated option names mid-word in the usage line.
:issue:`3362`
- :class:`ParamType` typing improvements. :pr:`3371`

- :class:`ParamType` is now a generic abstract base class,
Expand Down
15 changes: 14 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,12 +53,18 @@ 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: if ``False``, hyphenated tokens like
``--enable-verbose-logging`` won't be split
across lines. Defaults to ``True`` to match
:class:`textwrap.TextWrapper`.

.. versionchanged:: 8.4
Width is measured in visible characters. ANSI escape sequences in
``text``, ``initial_indent``, or ``subsequent_indent`` no longer
count toward the width budget, so styled input wraps based on what
the user sees instead of raw byte length.

Added the ``break_on_hyphens`` parameter.
"""
from ._textwrap import TextWrapper

Expand All @@ -67,6 +74,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 @@ -181,6 +189,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 @@ -190,7 +199,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
37 changes: 37 additions & 0 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,40 @@ 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_write_usage_does_not_break_options_at_hyphen():
"""Regression for #3362. Hyphenated option names on the usage line
used to wrap mid-word (e.g. ``--max-`` and ``retry-count`` on two
lines), because ``textwrap.TextWrapper`` defaults to
``break_on_hyphens=True``.
"""
options = [
"--enable-verbose-logging",
"--output-file-path",
"--max-retry-count",
"--disable-cache-mode",
"--config-file-location",
]
f = click.HelpFormatter(width=65)
f.write_usage("program", " ".join(options))

for line in f.getvalue().splitlines():
# No line should end with a hyphen, which is what would happen if
# an option name was split. A trailing space is fine.
assert not line.rstrip().endswith("-"), line


def test_wrap_text_break_on_hyphens_opt_in():
"""``wrap_text`` keeps the historical behavior by default and only
suppresses hyphen breaks when the caller opts in. Width 25 is wide
enough to almost fit ``--alpha-beta --gamma-delta``, so the default
wrapper splits the trailing word at its hyphen.
"""
text = "--alpha-beta --gamma-delta"
broken = click.formatting.wrap_text(text, width=25)
intact = click.formatting.wrap_text(text, width=25, break_on_hyphens=False)

assert any(line.endswith("-") for line in broken.splitlines())
assert not any(line.endswith("-") for line in intact.splitlines())
assert "--gamma-delta" in intact