diff --git a/CHANGES.rst b/CHANGES.rst index 506877590..6395bf578 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,17 @@ .. currentmodule:: click +Version 8.3.4 +------------- + +Unreleased + +- ``HelpFormatter.write_usage`` no longer breaks long ``--option-name`` flags + at an internal hyphen when the full token would fit on the next line. The + underlying :func:`wrap_text` helper gained a ``break_on_hyphens`` keyword + so callers can opt in or out, mirroring :class:`textwrap.TextWrapper`. + :issue:`3362` + + Version 8.3.3 ------------- diff --git a/src/click/formatting.py b/src/click/formatting.py index 0b64f831b..efc9b45b1 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -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 @@ -52,6 +53,12 @@ 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: passed through to :class:`textwrap.TextWrapper`. + Disable to keep hyphenated tokens (e.g. long ``--option-name`` flags + in usage lines) intact. + + .. versionchanged:: 8.3.4 + Added the ``break_on_hyphens`` parameter. """ from ._textwrap import TextWrapper @@ -61,6 +68,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) @@ -167,6 +175,9 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N text_width, initial_indent=usage_prefix, subsequent_indent=indent, + # Hyphens inside usage tokens (e.g. long ``--option-name`` + # flags) are part of the option, not natural break points. + break_on_hyphens=False, ) ) else: @@ -176,7 +187,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, ) ) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index c79f6577f..443c44373 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -49,6 +49,38 @@ def cli(): ] +def test_usage_does_not_break_options_at_internal_hyphen(): + """Long ``--option-name`` flags should not be wrapped at an internal + hyphen when the whole token would fit on the next line. + + Reproduce: https://github.com/pallets/click/issues/3362 + """ + 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", + ] + formatter = click.HelpFormatter(width=65) + formatter.write_usage("program", " ".join(options)) + output = formatter.getvalue() + + # No line ends with a dangling hyphen mid-token. + for line in output.splitlines(): + stripped = line.rstrip() + assert not stripped.endswith("-"), f"line broken at hyphen: {line!r}" + + # And the original tokens survive intact. + for option in options: + assert option in output + + def test_wrapping_long_options_strings(runner): @click.group() def cli():