Describe the bug
The minimal reproducer from #2017 still fails on coverage 7.15.2. That issue was closed as
fixed in 7.10.3, but the exact four-line repro from it reproduces identically on 7.10.2,
7.10.3, 7.13.0 and 7.15.2 here, so I think one path was left uncovered rather than
regressed.
The root cause looks like this: the only atexit.register(self._atexit) call is in
_init_for_start() (coverage/control.py:654), which is reachable only via
Coverage.start(). A Coverage object that only reports — the coverage report CLI, or
a library caller — therefore never registers _atexit, and _atexit is the only place that
drains self._data_to_close with close(force=True) (control.py:752-753).
That matters because Coverage.report() calls _prepare_data_for_reporting()
(control.py:1079), which — only when [paths] is configured — creates an in-memory
CoverageData(no_disk=True), opens a shared-cache SQLite connection inside update(), and
appends it to _data_to_close. For no_disk data, SqliteDb.close()
(coverage/sqlitedb.py:84) is deliberately a no-op unless force=True. So the connection
is never closed, and Python reports it at finalization.
_init_data() (control.py:666) does append to _data_to_close, which I believe was part
of the #2017 fix, but it does not call atexit.register() — so for a never-started
Coverage the list is populated and never drained.
This is adjacent to #2138 (fixed in 7.13.5, and that fix is present here — _reset() does
now close(force=True)), but distinct: the connection leaked here is opened after that
reset, inside update().
To Reproduce
mkdir /tmp/covrepro && cd /tmp/covrepro
printf '[tool.coverage.paths]\nsource = [\n "dummy",\n]\n' > pyproject.toml
pip install coverage==7.15.2
python -Werror -m coverage report
Output:
No data to report.
Exception ignored while finalizing database connection <sqlite3.Connection object at 0x1027fe4d0>:
ResourceWarning: unclosed database in <sqlite3.Connection object at 0x1027fe4d0>
Exit code is 1 under -Werror, so this fails a -Werror CI job.
Removing pyproject.toml (i.e. no [paths]) makes it clean, which matches
@ionelmc's finding in pytest-dev/pytest-cov#694 that it happens "only if you use paths in
coverage config, or multiple reports."
Environment / matrix
- macOS 15 (Darwin 24.6.0), arm64
- coverage 7.15.2 (also 7.13.0, 7.10.3, 7.10.2 — all identical)
- Reproduces on CPython 3.13 and 3.14; not on 3.12
- No pytest involved in the repro above
Expected behavior
coverage report should close the in-memory databases it opens, regardless of whether
start() was ever called on the instance.
Additional context — why this shows up so often via pytest-cov
pytest-dev/pytest-cov#694 is the same bug seen from the outside. pytest-cov's
Central.finish() does self.cov = self.combining_cov (pytest_cov/engine.py:270), and
combining_cov is never start()ed — so the instance that performs all reporting has no
atexit handler, exactly as above. summary() calls report() twice (once for the total,
once for the terminal table), so users see exactly two warnings per run. @ionelmc concluded
there is nothing pytest-cov can do from its side and asked for coverage-side changes; this
issue is that ask, with a pytest-free reproducer.
A possible fix, if it doesn't conflict with the reasons the no_disk guard exists: have
_prepare_data_for_reporting() close the previous mapped data (or register the atexit
handler from _init_data() / __init__ rather than only from _init_for_start()).
Note on one thing I could not pin down
In the pytest-cov case the warning is printed even without -Werror. I verified that
warnings.filters at interpreter exit still contains ('ignore', None, ResourceWarning, None, 0) in both a reproducing and a non-reproducing run, so the visibility difference is
not filter-driven — but I did not identify what makes it visible there. Possibly pytest's
unraisable-exception handling (johnthagen notes in #694 that behavior changed in pytest
8.4.0). Not needed for the repro above, which is pytest-free.
Describe the bug
The minimal reproducer from #2017 still fails on coverage 7.15.2. That issue was closed as
fixed in 7.10.3, but the exact four-line repro from it reproduces identically on 7.10.2,
7.10.3, 7.13.0 and 7.15.2 here, so I think one path was left uncovered rather than
regressed.
The root cause looks like this: the only
atexit.register(self._atexit)call is in_init_for_start()(coverage/control.py:654), which is reachable only viaCoverage.start(). ACoverageobject that only reports — thecoverage reportCLI, ora library caller — therefore never registers
_atexit, and_atexitis the only place thatdrains
self._data_to_closewithclose(force=True)(control.py:752-753).That matters because
Coverage.report()calls_prepare_data_for_reporting()(
control.py:1079), which — only when[paths]is configured — creates an in-memoryCoverageData(no_disk=True), opens a shared-cache SQLite connection insideupdate(), andappends it to
_data_to_close. Forno_diskdata,SqliteDb.close()(
coverage/sqlitedb.py:84) is deliberately a no-op unlessforce=True. So the connectionis never closed, and Python reports it at finalization.
_init_data()(control.py:666) does append to_data_to_close, which I believe was partof the #2017 fix, but it does not call
atexit.register()— so for a never-startedCoveragethe list is populated and never drained.This is adjacent to #2138 (fixed in 7.13.5, and that fix is present here —
_reset()doesnow
close(force=True)), but distinct: the connection leaked here is opened after thatreset, inside
update().To Reproduce
Output:
Exit code is 1 under
-Werror, so this fails a-WerrorCI job.Removing
pyproject.toml(i.e. no[paths]) makes it clean, which matches@ionelmc's finding in pytest-dev/pytest-cov#694 that it happens "only if you use
pathsincoverage config, or multiple reports."
Environment / matrix
Expected behavior
coverage reportshould close the in-memory databases it opens, regardless of whetherstart()was ever called on the instance.Additional context — why this shows up so often via pytest-cov
pytest-dev/pytest-cov#694 is the same bug seen from the outside. pytest-cov's
Central.finish()doesself.cov = self.combining_cov(pytest_cov/engine.py:270), andcombining_covis neverstart()ed — so the instance that performs all reporting has noatexithandler, exactly as above.summary()callsreport()twice (once for the total,once for the terminal table), so users see exactly two warnings per run. @ionelmc concluded
there is nothing pytest-cov can do from its side and asked for coverage-side changes; this
issue is that ask, with a pytest-free reproducer.
A possible fix, if it doesn't conflict with the reasons the
no_diskguard exists: have_prepare_data_for_reporting()close the previous mapped data (or register theatexithandler from
_init_data()/__init__rather than only from_init_for_start()).Note on one thing I could not pin down
In the pytest-cov case the warning is printed even without
-Werror. I verified thatwarnings.filtersat interpreter exit still contains('ignore', None, ResourceWarning, None, 0)in both a reproducing and a non-reproducing run, so the visibility difference isnot filter-driven — but I did not identify what makes it visible there. Possibly pytest's
unraisable-exception handling (johnthagen notes in #694 that behavior changed in pytest
8.4.0). Not needed for the repro above, which is pytest-free.