From 70511f36db0c8ca22140db78b60fe05c518cb48b Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 29 May 2021 22:40:19 +0200 Subject: [PATCH 01/13] bpo-42213: Remove redundant cyclic GC hack in sqlite3 The sqlite3 module now fully implements the GC protocol, so there's no need for this workaround anymore. --- Modules/_sqlite/cache.c | 11 ++--------- Modules/_sqlite/cache.h | 4 ---- Modules/_sqlite/connection.c | 8 -------- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/Modules/_sqlite/cache.c b/Modules/_sqlite/cache.c index fd4e619f6a0115..8196e3c5783727 100644 --- a/Modules/_sqlite/cache.c +++ b/Modules/_sqlite/cache.c @@ -97,9 +97,6 @@ pysqlite_cache_init(pysqlite_Cache *self, PyObject *args, PyObject *kwargs) } self->factory = Py_NewRef(factory); - - self->decref_factory = 1; - return 0; } @@ -108,9 +105,7 @@ cache_traverse(pysqlite_Cache *self, visitproc visit, void *arg) { Py_VISIT(Py_TYPE(self)); Py_VISIT(self->mapping); - if (self->decref_factory) { - Py_VISIT(self->factory); - } + Py_VISIT(self->factory); pysqlite_Node *node = self->first; while (node) { @@ -124,9 +119,7 @@ static int cache_clear(pysqlite_Cache *self) { Py_CLEAR(self->mapping); - if (self->decref_factory) { - Py_CLEAR(self->factory); - } + Py_CLEAR(self->factory); /* iterate over all nodes and deallocate them */ pysqlite_Node *node = self->first; diff --git a/Modules/_sqlite/cache.h b/Modules/_sqlite/cache.h index 083356f93f9e4c..209c80dcd54ad4 100644 --- a/Modules/_sqlite/cache.h +++ b/Modules/_sqlite/cache.h @@ -52,10 +52,6 @@ typedef struct pysqlite_Node* first; pysqlite_Node* last; - - /* if set, decrement the factory function when the Cache is deallocated. - * this is almost always desirable, but not in the pysqlite context */ - int decref_factory; } pysqlite_Cache; extern PyTypeObject *pysqlite_NodeType; diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index 7252ccab10b4bc..e7f0f7e4b15f2c 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -149,14 +149,6 @@ pysqlite_connection_init(pysqlite_Connection *self, PyObject *args, return -1; } - /* By default, the Cache class INCREFs the factory in its initializer, and - * decrefs it in its deallocator method. Since this would create a circular - * reference here, we're breaking it by decrementing self, and telling the - * cache class to not decref the factory (self) in its deallocator. - */ - self->statement_cache->decref_factory = 0; - Py_DECREF(self); - self->detect_types = detect_types; self->timeout = timeout; (void)sqlite3_busy_timeout(self->db, (int)(timeout*1000)); From 6cb435b8dacd39b3e0dc04b2280b5ddfb48e4af9 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 11:55:01 +0200 Subject: [PATCH 02/13] Split open tests into their own test case, and cleanup more explicitly --- Lib/sqlite3/test/dbapi.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index 39c9bf5b61143d..b9d13dfe1be3b1 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -170,10 +170,16 @@ def test_in_transaction_ro(self): with self.assertRaises(AttributeError): self.cx.in_transaction = True + +class OpenTests(unittest.TestCase): + def tearDown(self): + import gc + gc.collect() + unlink(TESTFN) + def test_open_with_path_like_object(self): """ Checks that we can successfully connect to a database using an object that is PathLike, i.e. has __fspath__(). """ - self.addCleanup(unlink, TESTFN) class Path: def __fspath__(self): return TESTFN @@ -182,7 +188,6 @@ def __fspath__(self): cx.execute('create table test(id integer)') def test_open_uri(self): - self.addCleanup(unlink, TESTFN) with sqlite.connect(TESTFN) as cx: cx.execute('create table test(id integer)') with sqlite.connect('file:' + TESTFN, uri=True) as cx: @@ -942,6 +947,7 @@ def suite(): CursorTests, ExtensionTests, ModuleTests, + OpenTests, SqliteOnConflictTests, ThreadTests, ] From 1e3f5898ac9a29da16b0713f3a344fbadea3c915 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 12:28:32 +0200 Subject: [PATCH 03/13] clean up test_trace_callback_content explicitly --- Lib/sqlite3/test/hooks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index 8c60bdcf5d70aa..77681f2f6c68d9 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -261,6 +261,11 @@ def trace(statement): cur.execute(queries[1]) self.assertEqual(traced_statements, queries) + con1.close() + con2.close() + import gc + gc.collect() + def suite(): tests = [ From 5ff1a806d22eec8ad09013108e2de76f0d9a55c9 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 12:35:13 +0200 Subject: [PATCH 04/13] Document workarounds --- Lib/sqlite3/test/dbapi.py | 1 + Lib/sqlite3/test/hooks.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index b9d13dfe1be3b1..aa894080fa8831 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -173,6 +173,7 @@ def test_in_transaction_ro(self): class OpenTests(unittest.TestCase): def tearDown(self): + # bpo-42213: ensure that TESTFN is closed before unlinking import gc gc.collect() unlink(TESTFN) diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index 77681f2f6c68d9..172c78b52b25e1 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -261,6 +261,7 @@ def trace(statement): cur.execute(queries[1]) self.assertEqual(traced_statements, queries) + # bpo-42213: ensure that TESTFN is closed before the cleanup runs con1.close() con2.close() import gc From 866e226c87558dd26e3ba309b8f5c801c2366734 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 13:42:06 +0200 Subject: [PATCH 05/13] Use test.support.gc_collect iso. gc.collect --- Lib/sqlite3/test/dbapi.py | 4 ++-- Lib/sqlite3/test/hooks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index aa894080fa8831..da82211175d353 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -25,6 +25,7 @@ import sqlite3 as sqlite import sys +from test.support import gc_collect from test.support.os_helper import TESTFN, unlink @@ -174,8 +175,7 @@ def test_in_transaction_ro(self): class OpenTests(unittest.TestCase): def tearDown(self): # bpo-42213: ensure that TESTFN is closed before unlinking - import gc - gc.collect() + gc_collect() unlink(TESTFN) def test_open_with_path_like_object(self): diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index 172c78b52b25e1..d8118d352dd764 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -23,6 +23,7 @@ import unittest import sqlite3 as sqlite +from test.support import gc_collect from test.support.os_helper import TESTFN, unlink @@ -264,8 +265,7 @@ def trace(statement): # bpo-42213: ensure that TESTFN is closed before the cleanup runs con1.close() con2.close() - import gc - gc.collect() + gc_collect() def suite(): From 063878cb35406d733bdf87a293eb64f221db767c Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 13:52:34 +0200 Subject: [PATCH 06/13] Revert gc workarounds Reverts commits: - 866e226c87558dd26e3ba309b8f5c801c2366734. - 5ff1a806d22eec8ad09013108e2de76f0d9a55c9. - 1e3f5898ac9a29da16b0713f3a344fbadea3c915. - 6cb435b8dacd39b3e0dc04b2280b5ddfb48e4af9. --- Lib/sqlite3/test/dbapi.py | 11 ++--------- Lib/sqlite3/test/hooks.py | 6 ------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index da82211175d353..39c9bf5b61143d 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -25,7 +25,6 @@ import sqlite3 as sqlite import sys -from test.support import gc_collect from test.support.os_helper import TESTFN, unlink @@ -171,16 +170,10 @@ def test_in_transaction_ro(self): with self.assertRaises(AttributeError): self.cx.in_transaction = True - -class OpenTests(unittest.TestCase): - def tearDown(self): - # bpo-42213: ensure that TESTFN is closed before unlinking - gc_collect() - unlink(TESTFN) - def test_open_with_path_like_object(self): """ Checks that we can successfully connect to a database using an object that is PathLike, i.e. has __fspath__(). """ + self.addCleanup(unlink, TESTFN) class Path: def __fspath__(self): return TESTFN @@ -189,6 +182,7 @@ def __fspath__(self): cx.execute('create table test(id integer)') def test_open_uri(self): + self.addCleanup(unlink, TESTFN) with sqlite.connect(TESTFN) as cx: cx.execute('create table test(id integer)') with sqlite.connect('file:' + TESTFN, uri=True) as cx: @@ -948,7 +942,6 @@ def suite(): CursorTests, ExtensionTests, ModuleTests, - OpenTests, SqliteOnConflictTests, ThreadTests, ] diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index d8118d352dd764..8c60bdcf5d70aa 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -23,7 +23,6 @@ import unittest import sqlite3 as sqlite -from test.support import gc_collect from test.support.os_helper import TESTFN, unlink @@ -262,11 +261,6 @@ def trace(statement): cur.execute(queries[1]) self.assertEqual(traced_statements, queries) - # bpo-42213: ensure that TESTFN is closed before the cleanup runs - con1.close() - con2.close() - gc_collect() - def suite(): tests = [ From b53aacbc60abe110c4fd144470ed3c29783718f3 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 20:32:38 +0200 Subject: [PATCH 07/13] Address review: Explicitly close opened database files --- Lib/sqlite3/test/dbapi.py | 14 ++++++++++++-- Lib/sqlite3/test/hooks.py | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index 39c9bf5b61143d..2af08fb7c73bb2 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -170,26 +170,35 @@ def test_in_transaction_ro(self): with self.assertRaises(AttributeError): self.cx.in_transaction = True + +class OpenTests(unittest.TestCase): + def tearDown(self): + unlink(TESTFN) + def test_open_with_path_like_object(self): """ Checks that we can successfully connect to a database using an object that is PathLike, i.e. has __fspath__(). """ - self.addCleanup(unlink, TESTFN) class Path: def __fspath__(self): return TESTFN path = Path() with sqlite.connect(path) as cx: cx.execute('create table test(id integer)') + cx.close() def test_open_uri(self): - self.addCleanup(unlink, TESTFN) with sqlite.connect(TESTFN) as cx: cx.execute('create table test(id integer)') + cx.close() + with sqlite.connect('file:' + TESTFN, uri=True) as cx: cx.execute('insert into test(id) values(0)') + cx.close() + with sqlite.connect('file:' + TESTFN + '?mode=ro', uri=True) as cx: with self.assertRaises(sqlite.OperationalError): cx.execute('insert into test(id) values(1)') + cx.close() class CursorTests(unittest.TestCase): @@ -942,6 +951,7 @@ def suite(): CursorTests, ExtensionTests, ModuleTests, + OpenTests, SqliteOnConflictTests, ThreadTests, ] diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index 8c60bdcf5d70aa..6685342cb90151 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -259,6 +259,8 @@ def trace(statement): cur.execute(queries[0]) con2.execute("create table bar(x)") cur.execute(queries[1]) + con1.close() + con2.close() self.assertEqual(traced_statements, queries) From e0ffe7664f6da6ce7f8e91f786a4d2157f6b9ca2 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 20:37:39 +0200 Subject: [PATCH 08/13] Try to harden connection close - add wrapper for sqlite3_close_v2() - explicitly free pending statements before close() --- Modules/_sqlite/connection.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index e7f0f7e4b15f2c..d3117dd2c2b684 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -250,6 +250,19 @@ connection_clear(pysqlite_Connection *self) return 0; } +static int +connection_close(pysqlite_Connection *self) +{ + int rc = SQLITE_OK; + + if (self->db) { + rc = sqlite3_close_v2(self->db); + self->db = NULL; + } + + return rc; +} + static void connection_dealloc(pysqlite_Connection *self) { @@ -258,9 +271,7 @@ connection_dealloc(pysqlite_Connection *self) tp->tp_clear((PyObject *)self); /* Clean up if user has not called .close() explicitly. */ - if (self->db) { - sqlite3_close_v2(self->db); - } + (void)connection_close(self); tp->tp_free(self); Py_DECREF(tp); @@ -345,22 +356,21 @@ static PyObject * pysqlite_connection_close_impl(pysqlite_Connection *self) /*[clinic end generated code: output=a546a0da212c9b97 input=3d58064bbffaa3d3]*/ { - int rc; - if (!pysqlite_check_thread(self)) { return NULL; } - pysqlite_do_all_statements(self, ACTION_FINALIZE, 1); - if (self->db) { - rc = sqlite3_close_v2(self->db); + /* Free pending statements before closing. This implies also cleaning + * up cursors, as they may have strong refs to statements. */ + Py_CLEAR(self->statement_cache); + Py_CLEAR(self->statements); + Py_CLEAR(self->cursors); + int rc = connection_close(self); if (rc != SQLITE_OK) { _pysqlite_seterror(self->db); return NULL; - } else { - self->db = NULL; } } From 4ebb593d5f95549223faa8804b64b617e0217af9 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 21:17:48 +0200 Subject: [PATCH 09/13] sqlite3_close_v2() always returns SQLITE_OK --- Modules/_sqlite/connection.c | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index d3117dd2c2b684..cef3b47901f83f 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -250,17 +250,14 @@ connection_clear(pysqlite_Connection *self) return 0; } -static int +static void connection_close(pysqlite_Connection *self) { - int rc = SQLITE_OK; - if (self->db) { - rc = sqlite3_close_v2(self->db); + int rc = sqlite3_close_v2(self->db); + assert(rc == SQLITE_OK); self->db = NULL; } - - return rc; } static void @@ -271,7 +268,7 @@ connection_dealloc(pysqlite_Connection *self) tp->tp_clear((PyObject *)self); /* Clean up if user has not called .close() explicitly. */ - (void)connection_close(self); + connection_close(self); tp->tp_free(self); Py_DECREF(tp); @@ -367,11 +364,7 @@ pysqlite_connection_close_impl(pysqlite_Connection *self) Py_CLEAR(self->statements); Py_CLEAR(self->cursors); - int rc = connection_close(self); - if (rc != SQLITE_OK) { - _pysqlite_seterror(self->db); - return NULL; - } + connection_close(self); } Py_RETURN_NONE; From 7d8014ab5b3672ee4994abecb7bc63578e56cde5 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 22:15:50 +0200 Subject: [PATCH 10/13] Check connection on __enter__ --- Modules/_sqlite/connection.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index cef3b47901f83f..42618d5e3216f8 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -1815,6 +1815,9 @@ static PyObject * pysqlite_connection_enter_impl(pysqlite_Connection *self) /*[clinic end generated code: output=457b09726d3e9dcd input=127d7a4f17e86d8f]*/ { + if (!pysqlite_check_connection(self)) { + return NULL; + } return Py_NewRef((PyObject *)self); } From 1b23df16d0dfad14b3f401ccbeeb34b7cbf3b82e Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 22:03:18 +0200 Subject: [PATCH 11/13] Add tests that exercise stuff against a closed database --- Lib/sqlite3/test/dbapi.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index 2af08fb7c73bb2..2b87520649132f 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -135,6 +135,25 @@ def test_failed_open(self): def test_close(self): self.cx.close() + def test_use_after_close(self): + sql = "select 1" + cu = self.cx.cursor() + res = cu.execute(sql) + self.cx.close() + self.assertRaises(sqlite.ProgrammingError, res.fetchall) + self.assertRaises(sqlite.ProgrammingError, cu.execute, sql) + self.assertRaises(sqlite.ProgrammingError, cu.executemany, sql, []) + self.assertRaises(sqlite.ProgrammingError, cu.executescript, sql) + self.assertRaises(sqlite.ProgrammingError, self.cx.execute, sql) + self.assertRaises(sqlite.ProgrammingError, + self.cx.executemany, sql, []) + self.assertRaises(sqlite.ProgrammingError, self.cx.executescript, sql) + self.assertRaises(sqlite.ProgrammingError, + self.cx.create_function, "t", 1, lambda x: x) + with self.assertRaises(sqlite.ProgrammingError): + with self.cx: + pass + def test_exceptions(self): # Optional DB-API extension. self.assertEqual(self.cx.Warning, sqlite.Warning) From d31403eb9885eb4654741a84b28f5dcd7179acad Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 22:44:34 +0200 Subject: [PATCH 12/13] Harden test clean up --- Lib/sqlite3/test/hooks.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/sqlite3/test/hooks.py b/Lib/sqlite3/test/hooks.py index 6685342cb90151..520a5b9f11cd40 100644 --- a/Lib/sqlite3/test/hooks.py +++ b/Lib/sqlite3/test/hooks.py @@ -254,13 +254,15 @@ def trace(statement): self.addCleanup(unlink, TESTFN) con1 = sqlite.connect(TESTFN, isolation_level=None) con2 = sqlite.connect(TESTFN) - con1.set_trace_callback(trace) - cur = con1.cursor() - cur.execute(queries[0]) - con2.execute("create table bar(x)") - cur.execute(queries[1]) - con1.close() - con2.close() + try: + con1.set_trace_callback(trace) + cur = con1.cursor() + cur.execute(queries[0]) + con2.execute("create table bar(x)") + cur.execute(queries[1]) + finally: + con1.close() + con2.close() self.assertEqual(traced_statements, queries) From aba8dd724618df1a3ab2cad90b03436cb9bc9fd8 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Wed, 2 Jun 2021 23:46:54 +0200 Subject: [PATCH 13/13] Add managed resource helper ...and sort imports --- Lib/sqlite3/test/dbapi.py | 44 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index 2b87520649132f..7a2c711c504ab8 100644 --- a/Lib/sqlite3/test/dbapi.py +++ b/Lib/sqlite3/test/dbapi.py @@ -20,14 +20,26 @@ # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. -import threading -import unittest +import contextlib import sqlite3 as sqlite import sys +import threading +import unittest from test.support.os_helper import TESTFN, unlink +# Helper for tests using TESTFN +@contextlib.contextmanager +def managed_connect(*args, **kwargs): + cx = sqlite.connect(*args, **kwargs) + try: + yield cx + finally: + cx.close() + unlink(TESTFN) + + class ModuleTests(unittest.TestCase): def test_api_level(self): self.assertEqual(sqlite.apilevel, "2.0", @@ -189,10 +201,8 @@ def test_in_transaction_ro(self): with self.assertRaises(AttributeError): self.cx.in_transaction = True - class OpenTests(unittest.TestCase): - def tearDown(self): - unlink(TESTFN) + _sql = "create table test(id integer)" def test_open_with_path_like_object(self): """ Checks that we can successfully connect to a database using an object that @@ -201,23 +211,17 @@ class Path: def __fspath__(self): return TESTFN path = Path() - with sqlite.connect(path) as cx: - cx.execute('create table test(id integer)') - cx.close() + with managed_connect(path) as cx: + cx.execute(self._sql) def test_open_uri(self): - with sqlite.connect(TESTFN) as cx: - cx.execute('create table test(id integer)') - cx.close() - - with sqlite.connect('file:' + TESTFN, uri=True) as cx: - cx.execute('insert into test(id) values(0)') - cx.close() - - with sqlite.connect('file:' + TESTFN + '?mode=ro', uri=True) as cx: - with self.assertRaises(sqlite.OperationalError): - cx.execute('insert into test(id) values(1)') - cx.close() + with managed_connect(TESTFN) as cx: + cx.execute(self._sql) + with managed_connect(f"file:{TESTFN}", uri=True) as cx: + cx.execute(self._sql) + with self.assertRaises(sqlite.OperationalError): + with managed_connect(f"file:{TESTFN}?mode=ro", uri=True) as cx: + cx.execute(self._sql) class CursorTests(unittest.TestCase):