diff --git a/datasette/app.py b/datasette/app.py index 16545cff..0f417ec9 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1819,6 +1819,7 @@ class Datasette: break except importlib.metadata.PackageNotFoundError: pass + conn.close() return info def _plugins(self, request=None, all=False): diff --git a/datasette/cli.py b/datasette/cli.py index 32a4d898..93aa22ef 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -615,7 +615,9 @@ def serve( for file in file_paths: if not pathlib.Path(file).exists(): if create: - sqlite3.connect(file).execute("vacuum") + conn = sqlite3.connect(file) + conn.execute("vacuum") + conn.close() else: raise click.ClickException( "Invalid value for '[FILES]...': Path '{}' does not exist.".format( diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 7fb81f02..1fea992e 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -681,13 +681,18 @@ def detect_fts_sql(table): def detect_json1(conn=None): + close_conn = False if conn is None: conn = sqlite3.connect(":memory:") + close_conn = True try: conn.execute("SELECT json('{}')") return True except Exception: return False + finally: + if close_conn: + conn.close() def table_columns(conn, table): diff --git a/datasette/utils/sqlite.py b/datasette/utils/sqlite.py index 342ff3fa..d0a2d783 100644 --- a/datasette/utils/sqlite.py +++ b/datasette/utils/sqlite.py @@ -20,15 +20,16 @@ def sqlite_version(): def _sqlite_version(): - return tuple( - map( - int, - sqlite3.connect(":memory:") - .execute("select sqlite_version()") - .fetchone()[0] - .split("."), + conn = sqlite3.connect(":memory:") + try: + return tuple( + map( + int, + conn.execute("select sqlite_version()").fetchone()[0].split("."), + ) ) - ) + finally: + conn.close() def supports_table_xinfo(): diff --git a/tests/conftest.py b/tests/conftest.py index 1a9b940f..3a3203fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,9 +100,10 @@ async def ds_client(): def pytest_report_header(config): - return "SQLite: {}".format( - sqlite3.connect(":memory:").execute("select sqlite_version()").fetchone()[0] - ) + conn = sqlite3.connect(":memory:") + version = conn.execute("select sqlite_version()").fetchone()[0] + conn.close() + return "SQLite: {}".format(version) def pytest_configure(config): diff --git a/tests/fixtures.py b/tests/fixtures.py index 713e6c17..f61ec0c7 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -187,6 +187,8 @@ def app_client(): def app_client_no_files(): ds = Datasette([]) yield TestClient(ds) + for db in ds.databases.values(): + db.close() @pytest.fixture(scope="session") @@ -822,6 +824,7 @@ def cli(db_filename, config, metadata, plugins_path, recreate, extra_db_filename for sql, params in TABLE_PARAMETERIZED_SQL: with conn: conn.execute(sql, params) + conn.close() print(f"Test tables written to {db_filename}") if metadata: with open(metadata, "w") as fp: @@ -850,6 +853,7 @@ def cli(db_filename, config, metadata, plugins_path, recreate, extra_db_filename pathlib.Path(extra_db_filename).unlink() conn = sqlite3.connect(extra_db_filename) conn.executescript(EXTRA_DATABASE_SQL) + conn.close() print(f"Test tables written to {extra_db_filename}") diff --git a/tests/test_api_write.py b/tests/test_api_write.py index e59c4295..adf8d310 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -17,6 +17,8 @@ def ds_write(tmp_path_factory): db.execute( "create table docs (id integer primary key, title text, score float, age integer)" ) + db1.close() + db2.close() ds = Datasette([db_path], immutables=[db_path_immutable]) ds.root_enabled = True yield ds diff --git a/tests/test_cli.py b/tests/test_cli.py index 7673c3f3..1d3a2b28 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -472,7 +472,9 @@ def test_serve_duplicate_database_names(tmpdir): nested.mkdir() db_2_path = str(tmpdir / "nested" / "db.db") for path in (db_1_path, db_2_path): - sqlite3.connect(path).execute("vacuum") + conn = sqlite3.connect(path) + conn.execute("vacuum") + conn.close() result = runner.invoke(cli, [db_1_path, db_2_path, "--get", "/-/databases.json"]) assert result.exit_code == 0, result.output databases = json.loads(result.output) @@ -486,7 +488,9 @@ def test_weird_database_names(tmpdir, filename): # https://github.com/simonw/datasette/issues/1181 runner = CliRunner() db_path = str(tmpdir / filename) - sqlite3.connect(db_path).execute("vacuum") + conn = sqlite3.connect(db_path) + conn.execute("vacuum") + conn.close() result1 = runner.invoke(cli, [db_path, "--get", "/"]) assert result1.exit_code == 0, result1.output filename_no_stem = filename.rsplit(".", 1)[0] @@ -523,7 +527,9 @@ def test_duplicate_database_files_error(tmpdir): """Test that passing the same database file multiple times raises an error""" runner = CliRunner() db_path = str(tmpdir / "test.db") - sqlite3.connect(db_path).execute("vacuum") + conn = sqlite3.connect(db_path) + conn.execute("vacuum") + conn.close() # Test with exact duplicate result = runner.invoke(cli, ["serve", db_path, db_path, "--get", "/"]) @@ -542,7 +548,9 @@ def test_duplicate_database_files_error(tmpdir): config_dir = tmpdir / "config" config_dir.mkdir() config_db_path = str(config_dir / "data.db") - sqlite3.connect(config_db_path).execute("vacuum") + conn = sqlite3.connect(config_db_path) + conn.execute("vacuum") + conn.close() result3 = runner.invoke( cli, ["serve", config_db_path, str(config_dir), "--get", "/"] @@ -553,7 +561,9 @@ def test_duplicate_database_files_error(tmpdir): # Test that mixing a file NOT in the directory with a directory works fine other_db_path = str(tmpdir / "other.db") - sqlite3.connect(other_db_path).execute("vacuum") + conn = sqlite3.connect(other_db_path) + conn.execute("vacuum") + conn.close() result4 = runner.invoke( cli, ["serve", other_db_path, str(config_dir), "--get", "/-/databases.json"] diff --git a/tests/test_config_dir.py b/tests/test_config_dir.py index ae7fe500..0a9b30d8 100644 --- a/tests/test_config_dir.py +++ b/tests/test_config_dir.py @@ -60,6 +60,7 @@ def config_dir(tmp_path_factory): (1, 'San Francisco') ; """) + db.close() # Mark "immutable.db" as immutable (config_dir / "inspect-data.json").write_text( @@ -95,6 +96,8 @@ def test_invalid_settings(config_dir): def config_dir_client(config_dir): ds = Datasette([], config_dir=config_dir) yield _TestClient(ds) + for db in ds.databases.values(): + db.close() def test_settings(config_dir_client): diff --git a/tests/test_crossdb.py b/tests/test_crossdb.py index 7807cd5d..11e53224 100644 --- a/tests/test_crossdb.py +++ b/tests/test_crossdb.py @@ -43,6 +43,7 @@ def test_crossdb_warning_if_too_many_databases(tmp_path_factory): path = str(db_dir / "db_{}.db".format(i)) conn = sqlite3.connect(path) conn.execute("vacuum") + conn.close() dbs.append(path) runner = CliRunner() result = runner.invoke( diff --git a/tests/test_internals_database.py b/tests/test_internals_database.py index 9a83dd4f..e3d35f57 100644 --- a/tests/test_internals_database.py +++ b/tests/test_internals_database.py @@ -543,7 +543,9 @@ async def test_execute_write_fn_exception(db): @pytest.mark.timeout(1) async def test_execute_write_fn_connection_exception(tmpdir, app_client): path = str(tmpdir / "immutable.db") - sqlite3.connect(path).execute("vacuum") + conn = sqlite3.connect(path) + conn.execute("vacuum") + conn.close() db = Database(app_client.ds, path=path, is_mutable=False) app_client.ds.add_database(db, name="immutable-db") @@ -747,15 +749,19 @@ async def test_replace_database(tmpdir): path1 = str(tmpdir / "data1.db") (tmpdir / "two").mkdir() path2 = str(tmpdir / "two" / "data1.db") - sqlite3.connect(path1).executescript(""" + conn1 = sqlite3.connect(path1) + conn1.executescript(""" create table t (id integer primary key); insert into t (id) values (1); insert into t (id) values (2); """) - sqlite3.connect(path2).executescript(""" + conn1.close() + conn2 = sqlite3.connect(path2) + conn2.executescript(""" create table t (id integer primary key); insert into t (id) values (1); """) + conn2.close() datasette = Datasette([path1]) db = datasette.get_database("data1") count = (await db.execute("select count(*) from t")).first()[0] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 083e23a0..7ebd57f3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -422,9 +422,9 @@ def test_plugins_async_template_function(restore_working_directory): .select("pre.extra_from_awaitable_function")[0] .text ) - expected = ( - sqlite3.connect(":memory:").execute("select sqlite_version()").fetchone()[0] - ) + conn = sqlite3.connect(":memory:") + expected = conn.execute("select sqlite_version()").fetchone()[0] + conn.close() assert expected == extra_from_awaitable_function @@ -466,6 +466,7 @@ def view_names_client(tmp_path_factory): db_path = str(tmpdir / "fixtures.db") conn = sqlite3.connect(db_path) conn.executescript(TABLES) + conn.close() return _TestClient( Datasette([db_path], template_dir=str(templates), plugins_dir=str(plugins)) ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 85ab9e6b..3fcb623e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -208,6 +208,7 @@ def test_detect_fts(open_quote, close_quote): assert None is utils.detect_fts(conn, "Test_View") assert None is utils.detect_fts(conn, "r") assert "Street_Tree_List_fts" == utils.detect_fts(conn, "Street_Tree_List") + conn.close() @pytest.mark.parametrize("table", ("regular", "has'single quote")) @@ -222,6 +223,7 @@ def test_detect_fts_different_table_names(table): conn = utils.sqlite3.connect(":memory:") conn.executescript(sql) assert "{table}_fts".format(table=table) == utils.detect_fts(conn, table) + conn.close() @pytest.mark.parametrize( @@ -359,6 +361,7 @@ def test_table_columns(): create table places (id integer primary key, name text, bob integer) """) assert ["id", "name", "bob"] == utils.table_columns(conn, "places") + conn.close() @pytest.mark.parametrize( @@ -433,11 +436,13 @@ def test_check_connection_spatialite_raises(): conn = sqlite3.connect(path) with pytest.raises(utils.SpatialiteConnectionProblem): utils.check_connection(conn) + conn.close() def test_check_connection_passes(): conn = sqlite3.connect(":memory:") utils.check_connection(conn) + conn.close() def test_call_with_supported_arguments(): @@ -564,10 +569,14 @@ def test_display_actor(actor, expected): async def test_initial_path_for_datasette(tmp_path_factory, dbs, expected_path): db_dir = tmp_path_factory.mktemp("dbs") one_table = str(db_dir / "one.db") - sqlite3.connect(one_table).execute("create table one (id integer primary key)") + conn1 = sqlite3.connect(one_table) + conn1.execute("create table one (id integer primary key)") + conn1.close() two_tables = str(db_dir / "two.db") - sqlite3.connect(two_tables).execute("create table two (id integer primary key)") - sqlite3.connect(two_tables).execute("create table three (id integer primary key)") + conn2 = sqlite3.connect(two_tables) + conn2.execute("create table two (id integer primary key)") + conn2.execute("create table three (id integer primary key)") + conn2.close() datasette = Datasette( [{"one_table": one_table, "two_tables": two_tables}[db] for db in dbs] ) diff --git a/tests/utils.py b/tests/utils.py index e2d9339a..808feea7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -34,7 +34,9 @@ def inner_html(soup): def has_load_extension(): conn = sqlite3.connect(":memory:") - return hasattr(conn, "enable_load_extension") + result = hasattr(conn, "enable_load_extension") + conn.close() + return result def cookie_was_deleted(response, cookie):