fix(cli): close bib read connections in comments extract; test store fixture (refs #615)

Fix round 1: the sealed-docket test file's _store() helper never closed
its in-memory Store, and extract's file-backed lookup connection was
never closed either — both leaked sqlite3 connections that surfaced as
nondeterministic ResourceWarnings when GC'd during unrelated tests.
Replace _store() with a store fixture that closes on teardown, and have
extract() close the private read connection _build_bib_helpers opens
for file-backed stores (the :memory: case still reuses store's own
connection, left untouched). Also reworded the _build_bib_helpers
docstring: attach_callback is None for --no-bib or --no-attach; the
store is None only for --no-bib.
This commit is contained in:
kert
2026-09-08 13:25:12 -04:00
parent 8b81beda14
commit 8453b8a1f9
2 changed files with 50 additions and 29 deletions

View File

@@ -40,9 +40,9 @@ def _build_bib_helpers(use_bib: bool, attach: bool):
``attach_callback(comment_id, comment_dir) -> None`` runs on the main
thread and registers a single ``"Comment text"`` note on the bib item
containing combined.md rendered to HTML. Skips items that already have
one. Both ``attach_callback`` and the store are ``None`` when
--no-bib or --no-attach (store is ``None`` only for --no-bib, so
``extract`` can still read seals with --no-attach).
one. ``attach_callback`` is ``None`` for --no-bib or --no-attach; the
store is ``None`` only for --no-bib, so ``extract`` can still read
seals with --no-attach.
"""
if not use_bib:
return (lambda _cid: ""), None, None
@@ -53,11 +53,13 @@ def _build_bib_helpers(use_bib: bool, attach: bool):
from bib import connect
store = connect()
own_con = False
if store._db_path == ":memory:": # noqa: SLF001
con = store._con() # noqa: SLF001 — tests only: a fresh connection would be an empty DB
else:
con = sqlite3.connect(store._db_path, check_same_thread=False) # noqa: SLF001
con.row_factory = sqlite3.Row
own_con = True
lock = threading.Lock()
def _row(comment_id: str):
@@ -71,6 +73,12 @@ def _build_bib_helpers(use_bib: bool, attach: bool):
row = _row(comment_id)
return (row["abstract"] or "") if row else ""
# extract() closes this on our behalf when it opened a private
# read connection (the file-backed branch above) — the :memory:
# connection is store's own and must outlive this call.
if own_con:
body_lookup.close = con.close
if not attach:
return body_lookup, None, store
@@ -153,18 +161,25 @@ def extract(
raise typer.Exit(1)
lookup, attach_cb, store = _build_bib_helpers(use_bib, attach=use_bib and attach)
skip = set(store.sealed_dockets()) if (store is not None and not force) else set()
stats = walk_and_extract(
root,
inline_body_lookup=lookup,
docket=docket,
limit=limit or None,
workers=workers,
force=force,
on_extracted=attach_cb,
skip_dockets=skip,
reattach=reattach,
)
try:
skip = (
set(store.sealed_dockets()) if (store is not None and not force) else set()
)
stats = walk_and_extract(
root,
inline_body_lookup=lookup,
docket=docket,
limit=limit or None,
workers=workers,
force=force,
on_extracted=attach_cb,
skip_dockets=skip,
reattach=reattach,
)
finally:
close = getattr(lookup, "close", None)
if close is not None:
close()
if docket and docket in skip:
typer.echo(f" {docket}: sealed; skipped — use --force")
for k, v in stats.items():

View File

@@ -4,6 +4,7 @@ from pathlib import Path
from unittest.mock import MagicMock, patch
import fitz
import pytest
from typer.testing import CliRunner
from bib.dockets import Docket
@@ -13,8 +14,13 @@ from cli.comments import app
runner = CliRunner()
def _store() -> Store:
return Store(":memory:", storage_dir="/tmp/nope")
@pytest.fixture
def store() -> Store:
s = Store(":memory:", storage_dir="/tmp/nope")
try:
yield s
finally:
s.close()
def _seed_dir(root: Path, docket: str) -> Path:
@@ -28,8 +34,8 @@ def _seed_dir(root: Path, docket: str) -> Path:
@patch("bib.connect")
def test_dockets_table_lists_rows(mc_connect):
s = _store()
def test_dockets_table_lists_rows(mc_connect, store):
s = store
s.docket_upsert(
Docket(
id="CMS-2019-0111",
@@ -52,8 +58,8 @@ def test_dockets_table_lists_rows(mc_connect):
@patch("bib.connect")
def test_seal_and_unseal(mc_connect):
s = _store()
def test_seal_and_unseal(mc_connect, store):
s = store
s.docket_upsert(Docket(id="CMS-2019-0111"))
mc_connect.return_value = s
assert (
@@ -68,8 +74,8 @@ def test_seal_and_unseal(mc_connect):
@patch("bib.connect")
def test_seal_unknown_docket_fails(mc_connect):
mc_connect.return_value = _store()
def test_seal_unknown_docket_fails(mc_connect, store):
mc_connect.return_value = store
result = runner.invoke(app, ["seal", "CMS-0000-0000"])
assert result.exit_code == 1
assert "unknown docket" in result.output
@@ -78,10 +84,10 @@ def test_seal_unknown_docket_fails(mc_connect):
@patch("bib.regulations_gov.discover_docket")
@patch("bib.regulations_gov.Client")
@patch("bib.connect")
def test_dockets_discover_populates_from_tags(mc_connect, mc_client, mc_disc):
def test_dockets_discover_populates_from_tags(mc_connect, mc_client, mc_disc, store):
from bib.item import Source
s = _store()
s = store
it = Source(title="c", url="https://www.regulations.gov/comment/CMS-2019-0111-1")
it.add_tag("reg-docket:CMS-2019-0111")
it.add_tag("rule:CMS-1693-P")
@@ -106,8 +112,8 @@ def test_dockets_discover_populates_from_tags(mc_connect, mc_client, mc_disc):
@patch("bib.connect")
def test_extract_skips_sealed_docket(mc_connect, tmp_path: Path):
s = _store()
def test_extract_skips_sealed_docket(mc_connect, tmp_path: Path, store):
s = store
s.docket_upsert(
Docket(
id="CMS-2024-0001", sealed_at="2025-01-01T00:00:00Z", seal_reason="manual"
@@ -124,8 +130,8 @@ def test_extract_skips_sealed_docket(mc_connect, tmp_path: Path):
@patch("bib.connect")
def test_extract_force_runs_sealed_docket(mc_connect, tmp_path: Path):
s = _store()
def test_extract_force_runs_sealed_docket(mc_connect, tmp_path: Path, store):
s = store
s.docket_upsert(Docket(id="CMS-2024-0001", sealed_at="2025-01-01T00:00:00Z"))
mc_connect.return_value = s
cdir = _seed_dir(tmp_path, "CMS-2024-0001")