Skip to content

config: preserve indentation and blank lines in help texts - #15032

Merged
RonnyPfannschmidt merged 2 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:fix/help-text-wrapping
Sep 15, 2026
Merged

RonnyPfannschmidt merged 2 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:fix/help-text-wrapping

Conversation

@RonnyPfannschmidt

Copy link
Copy Markdown
Member

AI-authored. I asked an agent (Claude Opus 5 via Claude Code) to look into
the help-text wrapping after reviewing #15021. The investigation, the
comparisons against other libraries, the patch, the tests and this text are
its work. I read it and I'm posting it.

Supersedes #15021, which fixes a narrower symptom on top of a base that is
itself wrong.

Background

87423d3 (#6817, @blueyed) taught the help formatter to keep explicit line
breaks, so that a help text can be written as a list instead of being reflowed
into one blob. It did that by splitting on newlines and wrapping each line
separately. That commit's own message names the cost:

This might also result in unexpected changes (hard wrapping), when line
endings where used unintentionally, e.g. with:

help="""
    some long
    help text
    """

The line.strip() in _split_lines is what holds that back. It works, but it
cannot distinguish source indentation from indentation that carries meaning, so
it flattens both:

control the thing                       control the thing
  * fast: skip the expensive...    →    * fast: skip the expensive validation
  * slow: run every check...            pass entirely and hope for the best

A wrapped item is now indistinguishable from a new one. Blank lines vanish too,
since textwrap.wrap("") returns no lines — that is the symptom #15021 reports.

The other half

There are two independent wrapping implementations. helpconfig.showhelp()
passes the whole help text to textwrap.wrap, so newlines degrade to ordinary
whitespace and the structure is lost completely. pytest's own ini option:

  parametrize_long_str_id_strategy (string):
                        strategy for long str/bytes parameter values in
                        auto-generated ids - short (default): values over 100
                        chars fall back to argname+index - sha256: replace value
                        with its sha256 hex digest - legacy: keep the full value
                        (for temporary backward compatibility) - disallow: raise
                        an error requesting explicit ids

Core has exactly one CLI option (--assert) and one ini option (that one) with
newlines in their help, so this path is where the only currently visible
breakage is.

This change

textwrap.dedent is the right tool for what strip() was reaching for: it
removes exactly the common source indentation, and keeps what is relative to it.
Each line then wraps carrying its own indent, and a list item's continuation
lines hang under the item's text. Both call sites share one _split_help_text,
so the ini path stops rotting separately.

  parametrize_long_str_id_strategy (string):
                        strategy for long str/bytes parameter values in
                        auto-generated ids
                        - short (default): values over 100 chars fall back to
                          argname+index
                        - sha256: replace value with its sha256 hex digest
                        - legacy: keep the full value (for temporary backward
                          compatibility)
                        - disallow: raise an error requesting explicit ids

Details worth a reviewer's eye:

  • The first line is stripped rather than dedented. It usually sits inline after
    the opening quotes, so it contributes no common prefix and plain dedent on
    the whole text would find "" and do nothing.
  • Blank lines are kept but trimmed at both ends, where they would only offset
    the text from the option it documents.
  • _format_action strips trailing whitespace, because argparse pads a blank
    help line out to the help column.
  • break_on_hyphens=False matches what showhelp already passed, and stops
    auto-generated splitting mid-word.

On reusing something instead

Checked, none fit:

  • stdlibtextwrap has only TextWrapper, which is single-paragraph by
    construction: initial_indent/subsequent_indent are two fixed strings for
    the whole call. argparse's RawTextHelpFormatter preserves structure but does
    not wrap at all.
  • clickwrap_text(preserve_paragraphs=True) sounds right and is not. It
    re-flows paragraph interiors, so pytest's bullet list collapses into exactly
    the run-on above; its \b escape disables wrapping outright, so items just
    overflow the terminal.
  • richText gets the model right, but gives continuation lines no indent
    (so it does not fix the actual bug), and is a large dependency for help
    formatting.

Checklist

  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits.
  • Create a new changelog file in the changelog directory.
  • AI tools were used; they are credited in a Co-authored-by commit trailer.

RonnyPfannschmidt and others added 2 commits September 15, 2026 18:56
87423d3 (pytest-dev#6817) made help texts keep their explicit line breaks by
splitting on newlines and wrapping each line separately. Its commit
message already named the cost:

    This might also result in unexpected changes (hard wrapping), when
    line endings where used unintentionally, e.g. with:

        help="""
            some long
            help text
            """

The `line.strip()` it used to contain that is too blunt: it cannot tell
source indentation from structure, so it also flattens the indentation
that carries meaning. A wrapped list item became indistinguishable from
a new item, and a blank line disappeared entirely because
`textwrap.wrap("")` returns no lines.

Use `textwrap.dedent` instead, which removes exactly the common source
indentation that the 2020 commit was worried about while keeping what is
relative to it. Each line then wraps with its own indent, continuation
lines of a list item hang under the item's text, and blank lines
survive.

Reuse the result for ini options too. `showhelp()` had a second, simpler
wrapping implementation that passed the whole help text to
`textwrap.wrap`, so newlines degraded to plain whitespace and the
structure was lost altogether -- visible in pytest's own
`parametrize_long_str_id_strategy`:

    parametrize_long_str_id_strategy (string):
                          strategy for long str/bytes parameter values in
                          auto-generated ids - short (default): values over 100
                          chars fall back to argname+index - sha256: replace value

which now renders as the list it was written as.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 15, 2026
@RonnyPfannschmidt
RonnyPfannschmidt merged commit a453d65 into pytest-dev:main Sep 15, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants