Skip to content

ResourceWarning: unclosed database still reproduces with [paths] config — a Coverage that only reports never registers its atexit cleanup #2251

Description

@spacecoffin

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions