https://github.com/python/cpython/commit/8e96dd6f25b53f0773d3f3f2a898eaa1501ee92e
commit: 8e96dd6f25b53f0773d3f3f2a898eaa1501ee92e
branch: main
author: Steve Stagg <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-21T07:58:49Z
summary:

gh-152817: Prevent deletion of sqlite3 `cursor.row_factory` attr, missed from: 
gh-149738 (GH-152818)

files:
M Lib/test/test_sqlite3/test_factory.py
M 
Misc/NEWS.d/next/Core_and_Builtins/2026-05-13-06-54-41.gh-issue-149738.4BLFoH.rst
M Modules/_sqlite/cursor.c

diff --git a/Lib/test/test_sqlite3/test_factory.py 
b/Lib/test/test_sqlite3/test_factory.py
index b9b18fdee87226..2dd42921d31ddd 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 e62b681d716650..e1935555b09174 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 3778ccd32fe4c4..96fb3dc9e42d3a 100644
--- a/Modules/_sqlite/cursor.c
+++ b/Modules/_sqlite/cursor.c
@@ -1405,13 +1405,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 *Py_UNUSED(closure))
+{
+    pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op);
+    return Py_NewRef(self->row_factory);
+}
+
+static int
+cursor_set_row_factory(PyObject *op, PyObject *value, void *Py_UNUSED(closure))
+{
+    pysqlite_Cursor *self = _pysqlite_Cursor_CAST(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},
 };
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to