From 62134f7feef04de19196299d12d7fd965e9d08f8 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 23 Dec 2022 14:40:33 +0300 Subject: [PATCH 1/4] gh-100450: Add `sqlite.Row.__contains__` method --- Doc/library/sqlite3.rst | 4 ++++ ...-12-23-14-40-16.gh-issue-100450.sevifI.rst | 1 + Modules/_sqlite/row.c | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-12-23-14-40-16.gh-issue-100450.sevifI.rst diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 3622864a4b06f9..c81b88831b9b40 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -2392,6 +2392,10 @@ Queries now return :class:`!Row` objects: 'Earth' >>> row["RADIUS"] # Column names are case-insensitive. 6378 + >>> 'name' in row + True + >>> 'missing_field' in row + False You can create a custom :attr:`~Cursor.row_factory` that returns each row as a :class:`dict`, with column names mapped to values: diff --git a/Misc/NEWS.d/next/Library/2022-12-23-14-40-16.gh-issue-100450.sevifI.rst b/Misc/NEWS.d/next/Library/2022-12-23-14-40-16.gh-issue-100450.sevifI.rst new file mode 100644 index 00000000000000..2925fa9a18c586 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-12-23-14-40-16.gh-issue-100450.sevifI.rst @@ -0,0 +1 @@ +Add ``sqlite.Row.__contains__`` method. diff --git a/Modules/_sqlite/row.c b/Modules/_sqlite/row.c index 1a1943285ce008..5d4676da5cfc7b 100644 --- a/Modules/_sqlite/row.c +++ b/Modules/_sqlite/row.c @@ -96,6 +96,25 @@ PyObject* pysqlite_row_item(pysqlite_Row* self, Py_ssize_t idx) return Py_XNewRef(item); } +static int pysqlite_row_contains(PyObject *self, PyObject *arg) +{ + Py_ssize_t nitems, i; + int cmp = 0; + pysqlite_Row *row = (pysqlite_Row *)self; + + nitems = PyTuple_Size(row->description); + + for (i = 0; cmp == 0 && i < nitems; i++) { + cmp = PyObject_RichCompareBool( + arg, + PyTuple_GET_ITEM(PyTuple_GET_ITEM(row->description, i), 0), + Py_EQ + ); + } + + return cmp; +} + static int equal_ignore_case(PyObject *left, PyObject *right) { @@ -249,6 +268,7 @@ static PyType_Slot row_slots[] = { {Py_mp_subscript, pysqlite_row_subscript}, {Py_sq_length, pysqlite_row_length}, {Py_sq_item, pysqlite_row_item}, + {Py_sq_contains, pysqlite_row_contains}, {Py_tp_new, pysqlite_row_new}, {Py_tp_traverse, row_traverse}, {Py_tp_clear, row_clear}, From 5b089ddb03b34cc15e1d3273068fbe211f230f8a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 23 Dec 2022 14:56:43 +0300 Subject: [PATCH 2/4] Remove cast --- Modules/_sqlite/row.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/_sqlite/row.c b/Modules/_sqlite/row.c index 5d4676da5cfc7b..da3da38e88e6b0 100644 --- a/Modules/_sqlite/row.c +++ b/Modules/_sqlite/row.c @@ -96,18 +96,18 @@ PyObject* pysqlite_row_item(pysqlite_Row* self, Py_ssize_t idx) return Py_XNewRef(item); } -static int pysqlite_row_contains(PyObject *self, PyObject *arg) +static int +pysqlite_row_contains(pysqlite_Row* self, PyObject *arg) { Py_ssize_t nitems, i; int cmp = 0; - pysqlite_Row *row = (pysqlite_Row *)self; - nitems = PyTuple_Size(row->description); + nitems = PyTuple_Size(self->description); for (i = 0; cmp == 0 && i < nitems; i++) { cmp = PyObject_RichCompareBool( arg, - PyTuple_GET_ITEM(PyTuple_GET_ITEM(row->description, i), 0), + PyTuple_GET_ITEM(PyTuple_GET_ITEM(self->description, i), 0), Py_EQ ); } From 251d4bff7d9caf27d2d2762bb90066221a945094 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 23 Dec 2022 19:55:31 +0300 Subject: [PATCH 3/4] Address review --- Modules/_sqlite/row.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Modules/_sqlite/row.c b/Modules/_sqlite/row.c index da3da38e88e6b0..30485591feeb8b 100644 --- a/Modules/_sqlite/row.c +++ b/Modules/_sqlite/row.c @@ -99,17 +99,16 @@ PyObject* pysqlite_row_item(pysqlite_Row* self, Py_ssize_t idx) static int pysqlite_row_contains(pysqlite_Row* self, PyObject *arg) { - Py_ssize_t nitems, i; + Py_ssize_t nitems; + Py_ssize_t i; + PyObject *key; int cmp = 0; nitems = PyTuple_Size(self->description); for (i = 0; cmp == 0 && i < nitems; i++) { - cmp = PyObject_RichCompareBool( - arg, - PyTuple_GET_ITEM(PyTuple_GET_ITEM(self->description, i), 0), - Py_EQ - ); + key = PyTuple_GET_ITEM(PyTuple_GET_ITEM(self->description, i), 0); + cmp = PyObject_RichCompareBool(arg, key, Py_EQ); } return cmp; From 5e419b548436c39487491be6377e4e4b82bbc4e6 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 24 Dec 2022 01:36:05 +0300 Subject: [PATCH 4/4] Add a unit test --- Doc/library/sqlite3.rst | 4 ---- Lib/test/test_sqlite3/test_factory.py | 8 ++++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index c81b88831b9b40..3622864a4b06f9 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -2392,10 +2392,6 @@ Queries now return :class:`!Row` objects: 'Earth' >>> row["RADIUS"] # Column names are case-insensitive. 6378 - >>> 'name' in row - True - >>> 'missing_field' in row - False You can create a custom :attr:`~Cursor.row_factory` that returns each row as a :class:`dict`, with column names mapped to values: diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py index 7fdc45ab69243d..b139ea267f29fc 100644 --- a/Lib/test/test_sqlite3/test_factory.py +++ b/Lib/test/test_sqlite3/test_factory.py @@ -240,6 +240,14 @@ def test_sqlite_row_as_sequence(self): self.assertEqual(list(reversed(row)), list(reversed(as_tuple))) self.assertIsInstance(row, Sequence) + def test_sqlite_row_contains(self): + """Checks if the row object can be used with `in`""" + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a, 2 as b").fetchone() + self.assertIn('a', row) + self.assertIn('b', row) + self.assertNotIn('c', row) + def test_fake_cursor_class(self): # Issue #24257: Incorrect use of PyObject_IsInstance() caused # segmentation fault.