From b6cd651ef489ed012479740f96076638fc25d9f1 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Thu, 14 Apr 2022 00:04:11 +0200 Subject: [PATCH 1/3] gh-69093: Add context manager support to sqlite3.Blob --- Doc/includes/sqlite3/blob_with.py | 12 +++++ Doc/library/sqlite3.rst | 4 ++ Lib/test/test_sqlite3/test_dbapi.py | 23 ++++++++ ...2-04-14-00-59-01.gh-issue-69093.bmlMwI.rst | 2 + Modules/_sqlite/blob.c | 43 +++++++++++++++ Modules/_sqlite/clinic/blob.c.h | 53 ++++++++++++++++++- 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 Doc/includes/sqlite3/blob_with.py create mode 100644 Misc/NEWS.d/next/Library/2022-04-14-00-59-01.gh-issue-69093.bmlMwI.rst diff --git a/Doc/includes/sqlite3/blob_with.py b/Doc/includes/sqlite3/blob_with.py new file mode 100644 index 00000000000000..d489bd632d867d --- /dev/null +++ b/Doc/includes/sqlite3/blob_with.py @@ -0,0 +1,12 @@ +import sqlite3 + +con = sqlite3.connect(":memory:") + +con.execute("create table test(blob_col blob)") +con.execute("insert into test(blob_col) values (zeroblob(10))") + +with con.blobopen("test", "blob_col", 1) as blob: + blob.write(b"Hello") + blob.write(b"World") + blob.seek(0) + print(blob.read()) # will print b"HelloWorld" diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index d0274fb79744d4..a47d672cb2072a 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1153,6 +1153,10 @@ Blob Objects .. literalinclude:: ../includes/sqlite3/blob.py + A :class:`Blob` can also be used as a :term:`context manager`: + + .. literalinclude:: ../includes/sqlite3/blob_with.py + .. _sqlite3-types: diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index faaa3713cb5107..39e57a3829e379 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1166,6 +1166,25 @@ def test_blob_sequence_not_supported(self): with self.assertRaises(TypeError): b"a" in self.blob + def test_blob_context_manager(self): + data = b"a" * 50 + with self.cx.blobopen("test", "b", 1) as blob: + blob.write(data) + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, data) + + # Check that __exit__ closed the blob + with self.assertRaisesRegex(sqlite.ProgrammingError, "closed blob"): + blob.read() + + def test_blob_context_manager_reraise_exceptions(self): + class DummyException(Exception): + pass + with self.assertRaisesRegex(DummyException, "reraised"): + with self.cx.blobopen("test", "b", 1) as blob: + raise DummyException("reraised") + + def test_blob_closed(self): with memory_database() as cx: cx.execute("create table test(b blob)") @@ -1182,6 +1201,10 @@ def test_blob_closed(self): blob.seek(0) with self.assertRaisesRegex(sqlite.ProgrammingError, msg): blob.tell() + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.__enter__() + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.__exit__(None, None, None) def test_blob_closed_db_read(self): with memory_database() as cx: diff --git a/Misc/NEWS.d/next/Library/2022-04-14-00-59-01.gh-issue-69093.bmlMwI.rst b/Misc/NEWS.d/next/Library/2022-04-14-00-59-01.gh-issue-69093.bmlMwI.rst new file mode 100644 index 00000000000000..d45a139b50e821 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-14-00-59-01.gh-issue-69093.bmlMwI.rst @@ -0,0 +1,2 @@ +Add :term:`context manager` support to :class:`sqlite3.Blob`. +Patch by Aviv Palivoda and Erlend E. Aasland. diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 821295cee813fd..aa244a5c6b7500 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -307,8 +307,51 @@ blob_tell_impl(pysqlite_Blob *self) } +/*[clinic input] +_sqlite3.Blob.__enter__ as blob_enter + +Blob context manager enter. +[clinic start generated code]*/ + +static PyObject * +blob_enter_impl(pysqlite_Blob *self) +/*[clinic end generated code: output=4fd32484b071a6cd input=fe4842c3c582d5a7]*/ +{ + if (!check_blob(self)) { + return NULL; + } + return Py_NewRef(self); +} + + +/*[clinic input] +_sqlite3.Blob.__exit__ as blob_exit + + type: object + val: object + tb: object + / + +Blob context manager exit. +[clinic start generated code]*/ + +static PyObject * +blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val, + PyObject *tb) +/*[clinic end generated code: output=fc86ceeb2b68c7b2 input=575d9ecea205f35f]*/ +{ + if (!check_blob(self)) { + return NULL; + } + close_blob(self); + Py_RETURN_FALSE; +} + + static PyMethodDef blob_methods[] = { BLOB_CLOSE_METHODDEF + BLOB_ENTER_METHODDEF + BLOB_EXIT_METHODDEF BLOB_READ_METHODDEF BLOB_SEEK_METHODDEF BLOB_TELL_METHODDEF diff --git a/Modules/_sqlite/clinic/blob.c.h b/Modules/_sqlite/clinic/blob.c.h index 30b3e3c194739f..237877a9b37f13 100644 --- a/Modules/_sqlite/clinic/blob.c.h +++ b/Modules/_sqlite/clinic/blob.c.h @@ -162,4 +162,55 @@ blob_tell(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored)) { return blob_tell_impl(self); } -/*[clinic end generated code: output=d3a02b127f2cfa58 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(blob_enter__doc__, +"__enter__($self, /)\n" +"--\n" +"\n" +"Blob context manager enter."); + +#define BLOB_ENTER_METHODDEF \ + {"__enter__", (PyCFunction)blob_enter, METH_NOARGS, blob_enter__doc__}, + +static PyObject * +blob_enter_impl(pysqlite_Blob *self); + +static PyObject * +blob_enter(pysqlite_Blob *self, PyObject *Py_UNUSED(ignored)) +{ + return blob_enter_impl(self); +} + +PyDoc_STRVAR(blob_exit__doc__, +"__exit__($self, type, val, tb, /)\n" +"--\n" +"\n" +"Blob context manager exit."); + +#define BLOB_EXIT_METHODDEF \ + {"__exit__", (PyCFunction)(void(*)(void))blob_exit, METH_FASTCALL, blob_exit__doc__}, + +static PyObject * +blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val, + PyObject *tb); + +static PyObject * +blob_exit(pysqlite_Blob *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *type; + PyObject *val; + PyObject *tb; + + if (!_PyArg_CheckPositional("__exit__", nargs, 3, 3)) { + goto exit; + } + type = args[0]; + val = args[1]; + tb = args[2]; + return_value = blob_exit_impl(self, type, val, tb); + +exit: + return return_value; +} +/*[clinic end generated code: output=ca2400862c18dadb input=a9049054013a1b77]*/ From ca093f3aae984581edc1fe1c68879f658dc1d039 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 15 Apr 2022 17:24:00 +0200 Subject: [PATCH 2/3] Try to consolidate examples and reword docs --- Doc/includes/sqlite3/blob.py | 3 +++ Doc/includes/sqlite3/blob_with.py | 12 ------------ Doc/library/sqlite3.rst | 7 +++---- 3 files changed, 6 insertions(+), 16 deletions(-) delete mode 100644 Doc/includes/sqlite3/blob_with.py diff --git a/Doc/includes/sqlite3/blob.py b/Doc/includes/sqlite3/blob.py index 61994fb82dd72a..6d5178185581fe 100644 --- a/Doc/includes/sqlite3/blob.py +++ b/Doc/includes/sqlite3/blob.py @@ -10,3 +10,6 @@ blob.seek(0) print(blob.read()) # will print b"HelloWorld" blob.close() + +with con.blobopen("test", "blob_col", 1) as blob: + blob.write(b"UpdateBlob") diff --git a/Doc/includes/sqlite3/blob_with.py b/Doc/includes/sqlite3/blob_with.py deleted file mode 100644 index d489bd632d867d..00000000000000 --- a/Doc/includes/sqlite3/blob_with.py +++ /dev/null @@ -1,12 +0,0 @@ -import sqlite3 - -con = sqlite3.connect(":memory:") - -con.execute("create table test(blob_col blob)") -con.execute("insert into test(blob_col) values (zeroblob(10))") - -with con.blobopen("test", "blob_col", 1) as blob: - blob.write(b"Hello") - blob.write(b"World") - blob.seek(0) - print(blob.read()) # will print b"HelloWorld" diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index a47d672cb2072a..12102eba235e3a 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1115,6 +1115,9 @@ Blob Objects data in an SQLite :abbr:`BLOB (Binary Large OBject)`. Call ``len(blob)`` to get the size (number of bytes) of the blob. + Use the :class:`Blob` as a :term:`context manager` to ensure that the blob + handle is closed after use. + .. method:: close() Close the blob. @@ -1153,10 +1156,6 @@ Blob Objects .. literalinclude:: ../includes/sqlite3/blob.py - A :class:`Blob` can also be used as a :term:`context manager`: - - .. literalinclude:: ../includes/sqlite3/blob_with.py - .. _sqlite3-types: From bce9c9fbda257021f464a2ce5e214cf40d66e223 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 15 Apr 2022 19:43:25 +0200 Subject: [PATCH 3/3] Simplify and relocate example --- Doc/includes/sqlite3/blob.py | 15 ++++++++------- Doc/library/sqlite3.rst | 6 ++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Doc/includes/sqlite3/blob.py b/Doc/includes/sqlite3/blob.py index 6d5178185581fe..b3694ad08af46b 100644 --- a/Doc/includes/sqlite3/blob.py +++ b/Doc/includes/sqlite3/blob.py @@ -4,12 +4,13 @@ con.execute("create table test(blob_col blob)") con.execute("insert into test(blob_col) values (zeroblob(10))") -blob = con.blobopen("test", "blob_col", 1) -blob.write(b"Hello") -blob.write(b"World") -blob.seek(0) -print(blob.read()) # will print b"HelloWorld" -blob.close() +# Write to our blob, using two write operations: +with con.blobopen("test", "blob_col", 1) as blob: + blob.write(b"Hello") + blob.write(b"World") +# Read the contents of our blob with con.blobopen("test", "blob_col", 1) as blob: - blob.write(b"UpdateBlob") + greeting = blob.read() + +print(greeting) # outputs "b'HelloWorld'" diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 12102eba235e3a..4838db01669e66 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1118,6 +1118,8 @@ Blob Objects Use the :class:`Blob` as a :term:`context manager` to ensure that the blob handle is closed after use. + .. literalinclude:: ../includes/sqlite3/blob.py + .. method:: close() Close the blob. @@ -1152,10 +1154,6 @@ Blob Objects current position) and :data:`os.SEEK_END` (seek relative to the blob’s end). - :class:`Blob` example: - - .. literalinclude:: ../includes/sqlite3/blob.py - .. _sqlite3-types: