Skip to content

Exclude ... bodies after multi-line return-type annotations - #2196

Merged
nedbat merged 2 commits into
coveragepy:mainfrom
mokashang:fix/2185-multiline-return-ellipsis-exclusion
Jun 21, 2026
Merged

nedbat merged 2 commits into
coveragepy:mainfrom
mokashang:fix/2185-multiline-return-ellipsis-exclusion

Conversation

@mokashang

Copy link
Copy Markdown
Contributor

The default exclusion regex for ... function bodies expects the def
signature to end with ). When a formatter (e.g. Ruff) splits a long
return-type annotation across multiple lines, the last line of the
signature ends with ] instead — for example:

class C:
    def f() -> dict[
        str, str
    ]: ...

With the current regex this function's body is reported as a missing
line, so coverage drops below 100% even though the body is just ....

This change widens the optional def-header group so the closing
bracket can be any (non-empty) run of ] and ). That lets the
regex still match:

  • def f(): ...
  • def f() -> int: ...
  • multi-line def signatures that close with ): ... (existing case)
  • multi-line def signatures that close with ]: ... (the bug)
  • multi-line def signatures that close with ]) -> int: ...
  • Tuple[int, ...] parameter types whose inner ... should not
    itself trigger the rule

It does not start matching unintended things: an annotated assignment
like x: list[int] = ... still doesn't match (no : after the ]),
and a regular function with a real body doesn't match either.

End-to-end reproducer from #2185:

$ cat example.py
class C:
    def f() -> dict[
        str, str
    ]: ...
$ coverage run --branch -m example && coverage report --fail-under=100

Before the change: example.py 3 0 2 1 80% (fail-under trips).
After: example.py 2 0 0 0 100%.

Testing

  • Added ExcludeTest::test_default_ellipsis_multiline_return_type covering
    single-line, split-return, split-param-and-return, and inner-Tuple[..., ...]
    shapes.
  • Existing ExcludeTest::test_default and the rest of ExcludeTest still
    pass (7/7).
  • tests/test_coverage.py tests/test_config.py tests/test_parser.py —
    233 passed, 7 skipped.
  • ruff format --check and mypy --strict on the changed files — clean.

Fixes #2185.

@nedbat

nedbat commented Jun 18, 2026

Copy link
Copy Markdown
Member

Thanks, I appreciate that you updated the changelog as well. The PR description includes things that should not be excluded. Please add those to the test to demonstrate that they are not matched.

You'll also need to resolve conflicts.

…overagepy#2185)

The default exclusion regex for ``...`` function bodies expected the
def signature to end with ``)``.  When a formatter (e.g. Ruff) splits a
long return-type annotation across multiple lines, the last line of the
signature ends with ``]`` instead — for example::

    def f() -> dict[
        str, str
    ]: ...

In that shape coverage.py reported the function body as a missing line
instead of excluding it.

Allow either ``)`` or ``]`` (and let them chain, e.g. ``])``) before
the optional ``-> return-type`` and the trailing ``: ...`` so the
exclusion applies regardless of how the signature is wrapped.  This
also keeps working: standalone ``...``, single-line ``def ...: ...``
declarations, and existing multi-line ``def`` signatures that close on
a line by themselves.

Adds a regression test exercising single-line, split-return, split-
param-and-return, and split-param-with-inner-``Tuple[..., ...]`` shapes.
@mokashang
mokashang force-pushed the fix/2185-multiline-return-ellipsis-exclusion branch from 348b209 to 62514df Compare June 19, 2026 16:08
@mokashang

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed an update:

  • Resolved the CHANGES.rst conflict by folding the entry into the existing Unreleased block beside the stdout/stderr mixup for implicit merge #2197 and sys.monitoring use_tool_id exception #2187 notes.
  • Extended test_default_ellipsis_multiline_return_type with two negative cases right under the stub block:
    • x: list[int] = ... — an annotated assignment whose RHS happens to be Ellipsis. It ends in ... and contains ], so it's the most obvious shape that could be over-matched by the widened pattern.
    • def with_body() -> dict[str, str]: return {} — a real one-line def whose return type contains ].

lines=[1, 15, 16, 17] now asserts that both of those, plus the trailing z = 17, remain executable statements (i.e. they are not excluded by the rule). I also re-ran the suite locally:

  • ExcludeTest — 7/7 pass
  • tests/test_coverage.py tests/test_config.py tests/test_parser.py — 233 passed, 7 skipped

(Re-checked the regex out of paranoia against a handful of other shapes that contain ... or ] — bare y = ..., y: int = ..., class C: ..., x = (1, 2, ...), foo[...] — none of them match.)

@nedbat
nedbat merged commit f4b2b4d into coveragepy:main Jun 21, 2026
42 checks passed
nedbat added a commit that referenced this pull request Jun 21, 2026
@nedbat

nedbat commented Jun 21, 2026

Copy link
Copy Markdown
Member

Thanks, what name should I put in the contributors list?

@mokashang

mokashang commented Jun 21, 2026 via email

Copy link
Copy Markdown
Contributor Author

@nedbat

nedbat commented Jun 22, 2026

Copy link
Copy Markdown
Member

This is now released as part of coverage 7.14.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Default ... exclusion rule misses when return type annotation is split over multiple lines

2 participants