Sitelet https://github.com/coveragepy/coveragepy/pull/2265
Skip to content

fix: annotate -d crashes on a file that is not .py - #2265

Merged
nedbat merged 3 commits into
coveragepy:mainfrom
VXNCXNX:fix/annotate-non-py-extension
Aug 18, 2026
Merged

nedbat merged 3 commits into
coveragepy:mainfrom
VXNCXNX:fix/annotate-non-py-extension

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

coverage annotate -d DIR crashes with a bare AssertionError when any measured
file has an extension other than .py.

A directory with run.py and a measured mod.pyw:

$ python -m coverage annotate -d ann
before:
  File ".../coverage/annotate.py", line 79, in annotate_file
    assert dest_file.endswith("_py")
AssertionError

after:
$ ls ann
mod.pyw,cover  run.py,cover

The same command without -d works, so this is specific to the directory path.

Cause

flat_rootname turns the dot of the extension into an underscore, so
mod.pyw becomes ..._mod_pyw, not ..._mod_py. annotate_file assumes
otherwise:

dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename()))
assert dest_file.endswith("_py")
dest_file = dest_file[:-3] + ".py"

That assert came from 84baf6e1, "refactor: remove an untaken branch in
annotate.py", which replaced an if dest_file.endswith("_py") guard with an
assertion. The branch was taken after all, just not by anything in the test
suite: a .pyw file, or a file measured by a plugin, reaches it.

The fix

Put the file's own extension back, whatever it is, instead of assuming .py.

Verification, in plain terms

I added test_annotate_dir_pyw, which measures a .pyw file and runs
annotate -d.

Then I put the buggy code back exactly as it reads on main today and re-ran
that test. It failed:

>           assert dest_file.endswith("_py")
E           AssertionError
coverage/annotate.py:79: AssertionError

I also tried a subtler wrong fix, hardcoding ".py" instead of reading the
file's real extension. That does not crash, it just writes the wrong filename,
and the test caught that too:

>       assert sorted(os.listdir("out_anno_dir")) == ["main.py,cover", "mod.pyw,cover"]
E       AssertionError: assert ['main.py,cover', 'mod_.py,cover'] == ['main.py,cover', 'mod.pyw,cover']

So the test fails both for the original bug and for a near-miss fix, and passes
only with the real one.

tests/test_annotate.py is 6 passed. ruff check and ruff format --check are
clean, no line over 100 characters.

On the rest of the suite: 194 failures before the change and the same 194 after,
with an empty diff between the two sorted lists. They are pre-existing in this
environment, which has no C extension built and no network.

The test runs on Linux: it exercises the reporting path for a .pyw file, which
is where the crash is, without needing Windows to execute one.

Disclosure: written with AI assistance (Claude Code). I reproduced the crash and the fix by running the real coverage annotate -d command against a measured .pyw file, and ran both checks above and the before/after failure-set comparison myself.

@nedbat
nedbat merged commit e1ace96 into coveragepy:main Aug 18, 2026
101 of 102 checks passed
@nedbat

nedbat commented Aug 28, 2026

Copy link
Copy Markdown
Member

This is now released as part of coverage 7.16.0.

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.

2 participants