From 38b2cdb16a5d588d770b936e2bb8b5616382b5ab Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 16 Apr 2022 09:07:41 +0200 Subject: [PATCH 1/9] gh-69093: Add mapping protocol support to sqlite3.Blob Authored-by: Aviv Palivoda Co-authored-by: Erlend E. Aasland --- Doc/library/sqlite3.rst | 7 +- Lib/test/test_sqlite3/test_dbapi.py | 102 +++++++++- ...2-04-14-01-00-31.gh-issue-69093.bmlMwI.rst | 2 + Modules/_sqlite/blob.c | 185 ++++++++++++++++++ 4 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 4838db01669e66..80b84a75df38bd 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1111,9 +1111,10 @@ Blob Objects .. class:: Blob - A :class:`Blob` instance is a :term:`file-like object` that can read and write - data in an SQLite :abbr:`BLOB (Binary Large OBject)`. Call ``len(blob)`` to - get the size (number of bytes) of the blob. + A :class:`Blob` instance is a :term:`file-like object` with + :term:`mapping` support, + that can read and write 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. diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 79dcb3ef8954a0..d225a321190dc6 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -33,7 +33,7 @@ check_disallow_instantiation, threading_helper, ) -from _testcapi import INT_MAX +from _testcapi import INT_MAX, ULLONG_MAX from os import SEEK_SET, SEEK_CUR, SEEK_END from test.support.os_helper import TESTFN, unlink, temp_dir @@ -1162,12 +1162,98 @@ def test_blob_open_error(self): with self.assertRaisesRegex(sqlite.OperationalError, regex): self.cx.blobopen(*args, **kwds) + def test_blob_length(self): + self.assertEqual(len(self.blob), 50) + + def test_blob_get_item(self): + self.assertEqual(self.blob[5], b"b") + self.assertEqual(self.blob[6], b"l") + self.assertEqual(self.blob[7], b"o") + self.assertEqual(self.blob[8], b"b") + self.assertEqual(self.blob[-1], b"!") + + def test_blob_set_item(self): + self.blob[0] = b"b" + expected = b"b" + self.data[1:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + def test_blob_set_item_negative_index(self): + self.blob[-1] = b"z" + self.assertEqual(self.blob[-1], b"z") + + def test_blob_get_slice(self): + self.assertEqual(self.blob[5:14], b"blob data") + + def test_blob_get_empty_slice(self): + self.assertEqual(self.blob[5:5], b"") + + def test_blob_get_slice_negative_index(self): + self.assertEqual(self.blob[5:-5], self.data[5:-5]) + + def test_blob_get_slice_with_skip(self): + self.assertEqual(self.blob[0:10:2], b"ti lb") + + def test_blob_set_slice(self): + self.blob[0:5] = b"12345" + expected = b"12345" + self.data[5:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + def test_blob_set_empty_slice(self): + self.blob[0:0] = b"" + self.assertEqual(self.blob[:], self.data) + + def test_blob_set_slice_with_skip(self): + self.blob[0:10:2] = b"12345" + actual = self.cx.execute("select b from test").fetchone()[0] + expected = b"1h2s3b4o5 " + self.data[10:] + self.assertEqual(actual, expected) + + def test_blob_mapping_invalid_index_type(self): + msg = "indices must be integers" + with self.assertRaisesRegex(TypeError, msg): + self.blob[5:5.5] + with self.assertRaisesRegex(TypeError, msg): + self.blob[1.5] + with self.assertRaisesRegex(TypeError, msg): + self.blob["a"] = b"b" + + def test_blob_get_item_error(self): + dataset = [len(self.blob), 105, -105] + for idx in dataset: + with self.subTest(idx=idx): + with self.assertRaisesRegex(IndexError, "index out of range"): + self.blob[idx] + with self.assertRaisesRegex(IndexError, "cannot fit 'int'"): + self.blob[ULLONG_MAX] + + def test_blob_set_item_error(self): + with self.assertRaisesRegex(ValueError, "must be a single byte"): + self.blob[0] = b"multiple" + with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"): + del self.blob[0] + with self.assertRaisesRegex(IndexError, "Blob index out of range"): + self.blob[1000] = b"a" + + def test_blob_set_slice_error(self): + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:10] = b"a" + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:10] = b"a" * 1000 + with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"): + del self.blob[5:10] + with self.assertRaisesRegex(ValueError, "step cannot be zero"): + self.blob[5:10:0] = b"12345" + with self.assertRaises(BufferError): + self.blob[5:10] = memoryview(b"abcde")[::2] + def test_blob_sequence_not_supported(self): - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, "unsupported operand"): self.blob + self.blob - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, "unsupported operand"): self.blob * 5 - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, "is not iterable"): b"a" in self.blob def test_blob_context_manager(self): @@ -1209,6 +1295,14 @@ def test_blob_closed(self): blob.__enter__() with self.assertRaisesRegex(sqlite.ProgrammingError, msg): blob.__exit__(None, None, None) + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + len(blob) + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0] + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0:1] + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0] = b"" def test_blob_closed_db_read(self): with memory_database() as cx: diff --git a/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst b/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst new file mode 100644 index 00000000000000..93e8196bd4a8e3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst @@ -0,0 +1,2 @@ +Add :term:`mapping` 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 3f766302d62517..a68d747f71aa89 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -347,6 +347,186 @@ blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val, Py_RETURN_FALSE; } +static Py_ssize_t +blob_length(pysqlite_Blob *self) +{ + if (!check_blob(self)) { + return -1; + } + return sqlite3_blob_bytes(self->blob); +}; + +static int +get_subscript_index(pysqlite_Blob *self, PyObject *item) +{ + Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError); + if (i == -1 && PyErr_Occurred()) { + return -1; + } + int blob_len = sqlite3_blob_bytes(self->blob); + if (i < 0) { + i += blob_len; + } + if (i < 0 || i >= blob_len) { + PyErr_SetString(PyExc_IndexError, "Blob index out of range"); + return -1; + } + return i; +} + +static PyObject * +subscript_index(pysqlite_Blob *self, PyObject *item) +{ + int i = get_subscript_index(self, item); + if (i < 0) { + return NULL; + } + return inner_read(self, 1, i); +} + +static int +get_slice_info(pysqlite_Blob *self, PyObject *item, Py_ssize_t *start, + Py_ssize_t *stop, Py_ssize_t *step, Py_ssize_t *slicelen) +{ + if (PySlice_Unpack(item, start, stop, step) < 0) { + return -1; + } + int len = sqlite3_blob_bytes(self->blob); + *slicelen = PySlice_AdjustIndices(len, start, stop, *step); + return 0; +} + +static PyObject * +subscript_slice(pysqlite_Blob *self, PyObject *item) +{ + Py_ssize_t start, stop, step, len; + if (get_slice_info(self, item, &start, &stop, &step, &len) < 0) { + return NULL; + } + + if (step == 1) { + return inner_read(self, len, start); + } + PyObject *blob = inner_read(self, stop - start, start); + if (blob == NULL) { + return NULL; + } + PyObject *result = PyBytes_FromStringAndSize(NULL, len); + if (result != NULL) { + char *blob_buf = PyBytes_AS_STRING(blob); + char *res_buf = PyBytes_AS_STRING(result); + for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) { + res_buf[i] = blob_buf[j]; + } + Py_DECREF(blob); + } + return result; +} + +static PyObject * +blob_subscript(pysqlite_Blob *self, PyObject *item) +{ + if (!check_blob(self)) { + return NULL; + } + + if (PyIndex_Check(item)) { + return subscript_index(self, item); + } + if (PySlice_Check(item)) { + return subscript_slice(self, item); + } + + PyErr_SetString(PyExc_TypeError, "Blob indices must be integers"); + return NULL; +} + +static int +ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "Blob doesn't support item deletion"); + return -1; + } + if (!PyBytes_Check(value) || PyBytes_Size(value) != 1) { + PyErr_SetString(PyExc_ValueError, + "Blob assignment must be a single byte"); + return -1; + } + + int i = get_subscript_index(self, item); + if (i < 0) { + return -1; + } + const char *buf = PyBytes_AS_STRING(value); + return inner_write(self, buf, 1, i); +} + +static int +ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "Blob doesn't support slice deletion"); + return -1; + } + + Py_ssize_t start, stop, step, len; + if (get_slice_info(self, item, &start, &stop, &step, &len) < 0) { + return -1; + } + + if (len == 0) { + return 0; + } + + Py_buffer vbuf; + if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) { + return -1; + } + + int rc = -1; + if (vbuf.len != len) { + PyErr_SetString(PyExc_IndexError, + "Blob slice assignment is wrong size"); + } + else if (step == 1) { + rc = inner_write(self, vbuf.buf, len, start); + } + else { + PyObject *blob_bytes = inner_read(self, stop - start, start); + if (blob_bytes != NULL) { + char *blob_buf = PyBytes_AS_STRING(blob_bytes); + for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) { + blob_buf[j] = ((char *)vbuf.buf)[i]; + } + rc = inner_write(self, blob_buf, stop - start, start); + Py_DECREF(blob_bytes); + } + } + PyBuffer_Release(&vbuf); + return rc; +} + +static int +blob_ass_subscript(pysqlite_Blob *self, PyObject *item, PyObject *value) +{ + if (!check_blob(self)) { + return -1; + } + + if (PyIndex_Check(item)) { + return ass_subscript_index(self, item, value); + } + if (PySlice_Check(item)) { + return ass_subscript_slice(self, item, value); + } + + PyErr_SetString(PyExc_TypeError, "Blob indices must be integers"); + return -1; +} + static PyMethodDef blob_methods[] = { BLOB_CLOSE_METHODDEF @@ -370,6 +550,11 @@ static PyType_Slot blob_slots[] = { {Py_tp_clear, blob_clear}, {Py_tp_methods, blob_methods}, {Py_tp_members, blob_members}, + + // Mapping protocol + {Py_mp_length, blob_length}, + {Py_mp_subscript, blob_subscript}, + {Py_mp_ass_subscript, blob_ass_subscript}, {0, NULL}, }; From 86e324a49243b7789e5254335b8dab22842c831f Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 16 Apr 2022 21:12:10 +0200 Subject: [PATCH 2/9] Cast --- Modules/_sqlite/blob.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index a68d747f71aa89..49d487fe6853a6 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -120,7 +120,7 @@ blob_seterror(pysqlite_Blob *self, int rc) } static PyObject * -inner_read(pysqlite_Blob *self, int length, int offset) +inner_read(pysqlite_Blob *self, Py_ssize_t length, Py_ssize_t offset) { PyObject *buffer = PyBytes_FromStringAndSize(NULL, length); if (buffer == NULL) { @@ -130,7 +130,7 @@ inner_read(pysqlite_Blob *self, int length, int offset) char *raw_buffer = PyBytes_AS_STRING(buffer); int rc; Py_BEGIN_ALLOW_THREADS - rc = sqlite3_blob_read(self->blob, raw_buffer, length, offset); + rc = sqlite3_blob_read(self->blob, raw_buffer, (int)length, (int)offset); Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { @@ -181,9 +181,11 @@ blob_read_impl(pysqlite_Blob *self, int length) }; static int -inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, int offset) +inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, + Py_ssize_t offset) { - int remaining_len = sqlite3_blob_bytes(self->blob) - self->offset; + int blob_len = sqlite3_blob_bytes(self->blob); + int remaining_len = blob_len - self->offset; if (len > remaining_len) { PyErr_SetString(PyExc_ValueError, "data longer than blob length"); return -1; @@ -191,7 +193,7 @@ inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, int offset) int rc; Py_BEGIN_ALLOW_THREADS - rc = sqlite3_blob_write(self->blob, buf, (int)len, offset); + rc = sqlite3_blob_write(self->blob, buf, (int)len, (int)offset); Py_END_ALLOW_THREADS if (rc != SQLITE_OK) { @@ -356,7 +358,7 @@ blob_length(pysqlite_Blob *self) return sqlite3_blob_bytes(self->blob); }; -static int +static Py_ssize_t get_subscript_index(pysqlite_Blob *self, PyObject *item) { Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError); @@ -377,7 +379,7 @@ get_subscript_index(pysqlite_Blob *self, PyObject *item) static PyObject * subscript_index(pysqlite_Blob *self, PyObject *item) { - int i = get_subscript_index(self, item); + Py_ssize_t i = get_subscript_index(self, item); if (i < 0) { return NULL; } @@ -455,7 +457,7 @@ ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value) return -1; } - int i = get_subscript_index(self, item); + Py_ssize_t i = get_subscript_index(self, item); if (i < 0) { return -1; } From 37a5c38c6ce5a922493e4cea7a8e5a9267263855 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sat, 16 Apr 2022 21:47:40 +0200 Subject: [PATCH 3/9] Assert lenght and offset --- Modules/_sqlite/blob.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 49d487fe6853a6..a4ad8d05158b7a 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -122,6 +122,9 @@ blob_seterror(pysqlite_Blob *self, int rc) static PyObject * inner_read(pysqlite_Blob *self, Py_ssize_t length, Py_ssize_t offset) { + assert(length <= sqlite3_blob_bytes(self->blob)); + assert(offset <= sqlite3_blob_bytes(self->blob) - self->offset); + PyObject *buffer = PyBytes_FromStringAndSize(NULL, length); if (buffer == NULL) { return NULL; @@ -191,6 +194,7 @@ inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, return -1; } + assert(offset <= remaining_len); int rc; Py_BEGIN_ALLOW_THREADS rc = sqlite3_blob_write(self->blob, buf, (int)len, (int)offset); From a1797495a901bc708b2227e6f6262e0d1ae1a174 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 17 Apr 2022 00:49:08 +0200 Subject: [PATCH 4/9] Accept all buffer objects --- Lib/test/test_sqlite3/test_dbapi.py | 9 +++++++++ Modules/_sqlite/blob.c | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index d225a321190dc6..bd1864535d2b6d 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1178,6 +1178,15 @@ def test_blob_set_item(self): actual = self.cx.execute("select b from test").fetchone()[0] self.assertEqual(actual, expected) + def test_blob_set_buffer_object(self): + from array import array + self.blob[0] = memoryview(b"1") + self.blob[0] = bytearray(b"1") + self.blob[0] = array("b", [1]) + self.blob[0:5] = memoryview(b"12345") + self.blob[0:5] = bytearray(b"12345") + self.blob[0:5] = array("b", [1, 2, 3, 4, 5]) + def test_blob_set_item_negative_index(self): self.blob[-1] = b"z" self.assertEqual(self.blob[-1], b"z") diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index a4ad8d05158b7a..2cda9514063fda 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -455,18 +455,24 @@ ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value) "Blob doesn't support item deletion"); return -1; } - if (!PyBytes_Check(value) || PyBytes_Size(value) != 1) { - PyErr_SetString(PyExc_ValueError, - "Blob assignment must be a single byte"); + Py_ssize_t i = get_subscript_index(self, item); + if (i < 0) { return -1; } - Py_ssize_t i = get_subscript_index(self, item); - if (i < 0) { + Py_buffer vbuf; + if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) { return -1; } - const char *buf = PyBytes_AS_STRING(value); - return inner_write(self, buf, 1, i); + int rc = -1; + if (vbuf.len != 1) { + PyErr_SetString(PyExc_ValueError, "Blob assignment must be a single byte"); + } + else { + rc = inner_write(self, (const char *)vbuf.buf, 1, i); + } + PyBuffer_Release(&vbuf); + return rc; } static int From ed6a9b62da79b991b3376f6d88ff95baf1d4ef6b Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 18 Apr 2022 23:57:35 +0200 Subject: [PATCH 5/9] Address review: reword docs and modify example --- Doc/includes/sqlite3/blob.py | 11 +++++++---- Doc/library/sqlite3.rst | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Doc/includes/sqlite3/blob.py b/Doc/includes/sqlite3/blob.py index b3694ad08af46b..d947059b3ae647 100644 --- a/Doc/includes/sqlite3/blob.py +++ b/Doc/includes/sqlite3/blob.py @@ -2,15 +2,18 @@ con = sqlite3.connect(":memory:") con.execute("create table test(blob_col blob)") -con.execute("insert into test(blob_col) values (zeroblob(10))") +con.execute("insert into test(blob_col) values (zeroblob(13))") # 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") + blob.write(b"hello, ") + blob.write(b"world.") + # Modify the first and last bytes of our blob + blob[0] = b"H" + blob[-1] = b"!" # Read the contents of our blob with con.blobopen("test", "blob_col", 1) as blob: greeting = blob.read() -print(greeting) # outputs "b'HelloWorld'" +print(greeting) # outputs "b'Hello, world!'" diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 4f2915540a2343..77be92a53862d8 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -1054,10 +1054,10 @@ Blob Objects .. class:: Blob - A :class:`Blob` instance is a :term:`file-like object` with - :term:`mapping` support, + A :class:`Blob` instance is a :term:`file-like object` that can read and write data in an SQLite :abbr:`BLOB (Binary Large OBject)`. - Call ``len(blob)`` to get the size (number of bytes) of the blob. + Call :func:`len(blob) ` to get the size (number of bytes) of the blob. + Use indices and :term:`slices ` for direct access to the blob data. Use the :class:`Blob` as a :term:`context manager` to ensure that the blob handle is closed after use. From 233f94074e9c31f5f260d3b890afc581b090d4c7 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Tue, 19 Apr 2022 00:04:25 +0200 Subject: [PATCH 6/9] bugfix: use correct offset in inner read/write --- Lib/test/test_sqlite3/test_dbapi.py | 16 ++++++++++++++++ Modules/_sqlite/blob.c | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index bd1864535d2b6d..07ee663655516d 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1138,6 +1138,13 @@ def test_blob_write_error_length(self): with self.assertRaisesRegex(ValueError, "data longer than blob"): self.blob.write(b"a" * 1000) + self.blob.seek(0, SEEK_SET) + n = len(self.blob) + self.blob.write(b"a" * (n-1)) + self.blob.write(b"a" * 1) + with self.assertRaisesRegex(ValueError, "data longer than blob"): + self.blob.write(b"a" * 1) + def test_blob_write_error_row_changed(self): self.cx.execute("update test set b='aaaa' where rowid=1") with self.assertRaises(sqlite.OperationalError): @@ -1178,6 +1185,15 @@ def test_blob_set_item(self): actual = self.cx.execute("select b from test").fetchone()[0] self.assertEqual(actual, expected) + def test_blob_set_item_with_offset(self): + self.blob.seek(0, SEEK_END) + self.assertEqual(self.blob.read(), b"") # verify that we're at EOB + self.blob[0] = b"T" + self.blob[-1] = b"." + self.blob.seek(0, SEEK_SET) + expected = b"This blob data string is exactly fifty bytes long." + self.assertEqual(self.blob.read(), expected) + def test_blob_set_buffer_object(self): from array import array self.blob[0] = memoryview(b"1") diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index 2cda9514063fda..ea12053a862c23 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -123,7 +123,7 @@ static PyObject * inner_read(pysqlite_Blob *self, Py_ssize_t length, Py_ssize_t offset) { assert(length <= sqlite3_blob_bytes(self->blob)); - assert(offset <= sqlite3_blob_bytes(self->blob) - self->offset); + assert(offset <= sqlite3_blob_bytes(self->blob)); PyObject *buffer = PyBytes_FromStringAndSize(NULL, length); if (buffer == NULL) { @@ -188,13 +188,13 @@ inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, Py_ssize_t offset) { int blob_len = sqlite3_blob_bytes(self->blob); - int remaining_len = blob_len - self->offset; + int remaining_len = blob_len - offset; if (len > remaining_len) { PyErr_SetString(PyExc_ValueError, "data longer than blob length"); return -1; } - assert(offset <= remaining_len); + assert(offset <= blob_len); int rc; Py_BEGIN_ALLOW_THREADS rc = sqlite3_blob_write(self->blob, buf, (int)len, (int)offset); From 9cbc33979006bc8a3c73d3d5fe63c57bcea07aa3 Mon Sep 17 00:00:00 2001 From: Erlend Egeberg Aasland Date: Tue, 19 Apr 2022 08:10:08 +0200 Subject: [PATCH 7/9] Update Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst Co-authored-by: Jelle Zijlstra --- .../next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst b/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst index 93e8196bd4a8e3..4bb8531beeacd8 100644 --- a/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst +++ b/Misc/NEWS.d/next/Library/2022-04-14-01-00-31.gh-issue-69093.bmlMwI.rst @@ -1,2 +1,2 @@ -Add :term:`mapping` support to :class:`sqlite3.Blob`. Patch by Aviv Palivoda +Add indexing and slicing support to :class:`sqlite3.Blob`. Patch by Aviv Palivoda and Erlend E. Aasland. From 7df4b9d394de8febbe3c5056baee4eccd623900a Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Tue, 19 Apr 2022 22:25:49 +0200 Subject: [PATCH 8/9] Address review: verify writes --- Lib/test/test_sqlite3/test_dbapi.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 07ee663655516d..8bfdce2bbe92e4 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -1141,9 +1141,9 @@ def test_blob_write_error_length(self): self.blob.seek(0, SEEK_SET) n = len(self.blob) self.blob.write(b"a" * (n-1)) - self.blob.write(b"a" * 1) + self.blob.write(b"a") with self.assertRaisesRegex(ValueError, "data longer than blob"): - self.blob.write(b"a" * 1) + self.blob.write(b"a") def test_blob_write_error_row_changed(self): self.cx.execute("update test set b='aaaa' where rowid=1") @@ -1197,11 +1197,22 @@ def test_blob_set_item_with_offset(self): def test_blob_set_buffer_object(self): from array import array self.blob[0] = memoryview(b"1") - self.blob[0] = bytearray(b"1") - self.blob[0] = array("b", [1]) + self.assertEqual(self.blob[0], b"1") + + self.blob[1] = bytearray(b"2") + self.assertEqual(self.blob[1], b"2") + + self.blob[2] = array("b", [4]) + self.assertEqual(self.blob[2], b"\x04") + self.blob[0:5] = memoryview(b"12345") - self.blob[0:5] = bytearray(b"12345") + self.assertEqual(self.blob[0:5], b"12345") + + self.blob[0:5] = bytearray(b"23456") + self.assertEqual(self.blob[0:5], b"23456") + self.blob[0:5] = array("b", [1, 2, 3, 4, 5]) + self.assertEqual(self.blob[0:5], b"\x01\x02\x03\x04\x05") def test_blob_set_item_negative_index(self): self.blob[-1] = b"z" From cb2619e4943fcdc718f20dbf82e37fdb10cd1cfd Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Tue, 19 Apr 2022 22:27:26 +0200 Subject: [PATCH 9/9] Address review: Py_ssize_t iso. int --- Modules/_sqlite/blob.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_sqlite/blob.c b/Modules/_sqlite/blob.c index ea12053a862c23..0c57ff8ca4252b 100644 --- a/Modules/_sqlite/blob.c +++ b/Modules/_sqlite/blob.c @@ -187,8 +187,8 @@ static int inner_write(pysqlite_Blob *self, const void *buf, Py_ssize_t len, Py_ssize_t offset) { - int blob_len = sqlite3_blob_bytes(self->blob); - int remaining_len = blob_len - offset; + Py_ssize_t blob_len = sqlite3_blob_bytes(self->blob); + Py_ssize_t remaining_len = blob_len - offset; if (len > remaining_len) { PyErr_SetString(PyExc_ValueError, "data longer than blob length"); return -1;