diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py index a9abeab31936880..26217763489e0f4 100644 --- a/Lib/test/test_sqlite3/test_factory.py +++ b/Lib/test/test_sqlite3/test_factory.py @@ -156,6 +156,14 @@ def test_delete_connection_text_factory(self): with self.assertRaises(AttributeError): del self.con.text_factory + def test_delete_cursor_row_factory(self): + # gh-149738: deleting row_factory should raise an exception + cur = self.con.cursor() + with self.assertRaises(AttributeError): + del cur.row_factory + # Executing a query here should succeed. + self.assertEqual(tuple(cur.execute("select 1").fetchone()), (1,)) + def test_sqlite_row_index_unicode(self): row = self.con.execute("select 1 as \xff").fetchone() self.assertEqual(row["\xff"], 1) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst index e62b681d716650b..e1935555b091742 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst @@ -1,2 +1,2 @@ :mod:`sqlite3`: Disallow removing ``row_factory`` and ``text_factory`` attributes -of a connection to prevent a crash on a query. +of a connection or cursor to prevent a crash on a query. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 5a61e43617984d9..e3a40ce73954307 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -1400,13 +1400,33 @@ static struct PyMemberDef cursor_members[] = {"description", _Py_T_OBJECT, offsetof(pysqlite_Cursor, description), Py_READONLY}, {"lastrowid", _Py_T_OBJECT, offsetof(pysqlite_Cursor, lastrowid), Py_READONLY}, {"rowcount", Py_T_LONG, offsetof(pysqlite_Cursor, rowcount), Py_READONLY}, - {"row_factory", _Py_T_OBJECT, offsetof(pysqlite_Cursor, row_factory), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(pysqlite_Cursor, in_weakreflist), Py_READONLY}, {NULL} }; +static PyObject * +cursor_get_row_factory(PyObject *op, void *closure) +{ + pysqlite_Cursor *self = (pysqlite_Cursor *)op; + return Py_NewRef(self->row_factory); +} + +static int +cursor_set_row_factory(PyObject *op, PyObject *value, void *closure) +{ + pysqlite_Cursor *self = (pysqlite_Cursor *)op; + if (value == NULL) { + PyErr_SetString(PyExc_AttributeError, + "cannot delete row_factory attribute"); + return -1; + } + Py_XSETREF(self->row_factory, Py_NewRef(value)); + return 0; +} + static struct PyGetSetDef cursor_getsets[] = { _SQLITE3_CURSOR_ARRAYSIZE_GETSETDEF + {"row_factory", cursor_get_row_factory, cursor_set_row_factory}, {NULL}, };