Database as a context manager, fixed many pytest warnings

* Database can now work as a context manager
* Claude Code helped fix a ton of .close() warnings

https://gistpreview.github.io/?730f0c5dc38528a1dd0615f330bd5481

* New autouse fixture to help with test warnings

Refs https://github.com/simonw/sqlite-utils/issues/692#issuecomment-3644371889

* Fix all remaining resource warnings

https://gistpreview.github.io/?0bb8e869b82f6ff0db647de755182502

Closes #692
This commit is contained in:
Simon Willison 2025-12-11 16:56:12 -08:00 committed by GitHub
commit fd5b09f64b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 231 additions and 66 deletions

View file

@ -14,6 +14,26 @@ def pytest_configure(config):
sys._called_from_test = True
@pytest.fixture(autouse=True)
def close_all_databases():
"""Automatically close all Database objects created during a test."""
databases = []
original_init = Database.__init__
def tracking_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
databases.append(self)
Database.__init__ = tracking_init
yield
Database.__init__ = original_init
for db in databases:
try:
db.close()
except Exception:
pass
@pytest.fixture
def fresh_db():
return Database(memory=True)
@ -38,4 +58,5 @@ def db_path(tmpdir):
path = str(tmpdir / "test.db")
db = sqlite3.connect(path)
db.executescript(CREATE_TABLES)
db.close()
return path

View file

@ -137,6 +137,7 @@ def db_to_analyze_path(db_to_analyze, tmpdir):
db = sqlite3.connect(path)
sql = "\n".join(db_to_analyze.iterdump())
db.executescript(sql)
db.close()
return path

View file

@ -19,9 +19,11 @@ def _supports_pragma_function_list():
db = Database(memory=True)
try:
db.execute("select * from pragma_function_list()")
return True
except Exception:
return False
return True
finally:
db.close()
def _has_compiled_ext():

View file

@ -328,7 +328,8 @@ def test_memory_return_db(tmpdir):
from sqlite_utils.cli import cli
path = str(tmpdir / "dogs.csv")
open(path, "w").write("id,name\n1,Cleo")
with open(path, "w") as f:
f.write("id,name\n1,Cleo")
with click.Context(cli) as ctx:
db = ctx.invoke(cli.commands["memory"], paths=(path,), return_db=True)

View file

@ -2,6 +2,14 @@ from sqlite_utils.db import Index, View, Database, XIndex, XIndexColumn
import pytest
def _check_supports_strict():
"""Check if SQLite supports strict tables without leaking the database."""
db = Database(memory=True)
result = db.supports_strict
db.close()
return result
def test_table_names(existing_db):
assert ["foo"] == existing_db.table_names()
@ -282,7 +290,7 @@ def test_use_rowid(fresh_db):
@pytest.mark.skipif(
not Database(memory=True).supports_strict,
not _check_supports_strict(),
reason="Needs SQLite version that supports strict",
)
@pytest.mark.parametrize(

View file

@ -9,9 +9,11 @@ def _supports_pragma_function_list():
db = Database(memory=True)
try:
db.execute("select * from pragma_function_list()")
return True
except Exception:
return False
return True
finally:
db.close()
def test_register_commands():

View file

@ -14,8 +14,11 @@ def test_recreate_ignored_for_in_memory():
def test_recreate_not_allowed_for_connection():
conn = sqlite3.connect(":memory:")
with pytest.raises(AssertionError):
Database(conn, recreate=True)
try:
with pytest.raises(AssertionError):
Database(conn, recreate=True)
finally:
conn.close()
@pytest.mark.parametrize(

View file

@ -42,39 +42,46 @@ def test_register_function_deterministic(fresh_db):
def test_register_function_deterministic_tries_again_if_exception_raised(fresh_db):
# Save the original connection so we can close it later
original_conn = fresh_db.conn
fresh_db.conn = MagicMock()
fresh_db.conn.create_function = MagicMock()
@fresh_db.register_function(deterministic=True)
def to_lower_2(s):
return s.lower()
try:
fresh_db.conn.create_function.assert_called_with(
"to_lower_2", 1, to_lower_2, deterministic=True
)
@fresh_db.register_function(deterministic=True)
def to_lower_2(s):
return s.lower()
first = True
fresh_db.conn.create_function.assert_called_with(
"to_lower_2", 1, to_lower_2, deterministic=True
)
def side_effect(*args, **kwargs):
# Raise exception only first time this is called
nonlocal first
if first:
first = False
raise sqlite3.NotSupportedError()
first = True
# But if sqlite3.NotSupportedError is raised, it tries again
fresh_db.conn.create_function.reset_mock()
fresh_db.conn.create_function.side_effect = side_effect
def side_effect(*args, **kwargs):
# Raise exception only first time this is called
nonlocal first
if first:
first = False
raise sqlite3.NotSupportedError()
@fresh_db.register_function(deterministic=True)
def to_lower_3(s):
return s.lower()
# But if sqlite3.NotSupportedError is raised, it tries again
fresh_db.conn.create_function.reset_mock()
fresh_db.conn.create_function.side_effect = side_effect
# Should have been called once with deterministic=True and once without
assert fresh_db.conn.create_function.call_args_list == [
call("to_lower_3", 1, to_lower_3, deterministic=True),
call("to_lower_3", 1, to_lower_3),
]
@fresh_db.register_function(deterministic=True)
def to_lower_3(s):
return s.lower()
# Should have been called once with deterministic=True and once without
assert fresh_db.conn.create_function.call_args_list == [
call("to_lower_3", 1, to_lower_3, deterministic=True),
call("to_lower_3", 1, to_lower_3),
]
finally:
# Close the original connection that was replaced with the mock
original_conn.close()
def test_register_function_replace(fresh_db):