Skip to content

Commit 38b2cdb

Browse files
author
Erlend E. Aasland
committed
pythongh-69093: Add mapping protocol support to sqlite3.Blob
Authored-by: Aviv Palivoda <[email protected]> Co-authored-by: Erlend E. Aasland <[email protected]>
1 parent f2bc12f commit 38b2cdb

File tree

4 files changed

+289
-7
lines changed

4 files changed

+289
-7
lines changed

Doc/library/sqlite3.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,9 +1111,10 @@ Blob Objects
11111111

11121112
.. class:: Blob
11131113

1114-
A :class:`Blob` instance is a :term:`file-like object` that can read and write
1115-
data in an SQLite :abbr:`BLOB (Binary Large OBject)`. Call ``len(blob)`` to
1116-
get the size (number of bytes) of the blob.
1114+
A :class:`Blob` instance is a :term:`file-like object` with
1115+
:term:`mapping` support,
1116+
that can read and write data in an SQLite :abbr:`BLOB (Binary Large OBject)`.
1117+
Call ``len(blob)`` to get the size (number of bytes) of the blob.
11171118

11181119
Use the :class:`Blob` as a :term:`context manager` to ensure that the blob
11191120
handle is closed after use.

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
check_disallow_instantiation,
3434
threading_helper,
3535
)
36-
from _testcapi import INT_MAX
36+
from _testcapi import INT_MAX, ULLONG_MAX
3737
from os import SEEK_SET, SEEK_CUR, SEEK_END
3838
from test.support.os_helper import TESTFN, unlink, temp_dir
3939

@@ -1162,12 +1162,98 @@ def test_blob_open_error(self):
11621162
with self.assertRaisesRegex(sqlite.OperationalError, regex):
11631163
self.cx.blobopen(*args, **kwds)
11641164

1165+
def test_blob_length(self):
1166+
self.assertEqual(len(self.blob), 50)
1167+
1168+
def test_blob_get_item(self):
1169+
self.assertEqual(self.blob[5], b"b")
1170+
self.assertEqual(self.blob[6], b"l")
1171+
self.assertEqual(self.blob[7], b"o")
1172+
self.assertEqual(self.blob[8], b"b")
1173+
self.assertEqual(self.blob[-1], b"!")
1174+
1175+
def test_blob_set_item(self):
1176+
self.blob[0] = b"b"
1177+
expected = b"b" + self.data[1:]
1178+
actual = self.cx.execute("select b from test").fetchone()[0]
1179+
self.assertEqual(actual, expected)
1180+
1181+
def test_blob_set_item_negative_index(self):
1182+
self.blob[-1] = b"z"
1183+
self.assertEqual(self.blob[-1], b"z")
1184+
1185+
def test_blob_get_slice(self):
1186+
self.assertEqual(self.blob[5:14], b"blob data")
1187+
1188+
def test_blob_get_empty_slice(self):
1189+
self.assertEqual(self.blob[5:5], b"")
1190+
1191+
def test_blob_get_slice_negative_index(self):
1192+
self.assertEqual(self.blob[5:-5], self.data[5:-5])
1193+
1194+
def test_blob_get_slice_with_skip(self):
1195+
self.assertEqual(self.blob[0:10:2], b"ti lb")
1196+
1197+
def test_blob_set_slice(self):
1198+
self.blob[0:5] = b"12345"
1199+
expected = b"12345" + self.data[5:]
1200+
actual = self.cx.execute("select b from test").fetchone()[0]
1201+
self.assertEqual(actual, expected)
1202+
1203+
def test_blob_set_empty_slice(self):
1204+
self.blob[0:0] = b""
1205+
self.assertEqual(self.blob[:], self.data)
1206+
1207+
def test_blob_set_slice_with_skip(self):
1208+
self.blob[0:10:2] = b"12345"
1209+
actual = self.cx.execute("select b from test").fetchone()[0]
1210+
expected = b"1h2s3b4o5 " + self.data[10:]
1211+
self.assertEqual(actual, expected)
1212+
1213+
def test_blob_mapping_invalid_index_type(self):
1214+
msg = "indices must be integers"
1215+
with self.assertRaisesRegex(TypeError, msg):
1216+
self.blob[5:5.5]
1217+
with self.assertRaisesRegex(TypeError, msg):
1218+
self.blob[1.5]
1219+
with self.assertRaisesRegex(TypeError, msg):
1220+
self.blob["a"] = b"b"
1221+
1222+
def test_blob_get_item_error(self):
1223+
dataset = [len(self.blob), 105, -105]
1224+
for idx in dataset:
1225+
with self.subTest(idx=idx):
1226+
with self.assertRaisesRegex(IndexError, "index out of range"):
1227+
self.blob[idx]
1228+
with self.assertRaisesRegex(IndexError, "cannot fit 'int'"):
1229+
self.blob[ULLONG_MAX]
1230+
1231+
def test_blob_set_item_error(self):
1232+
with self.assertRaisesRegex(ValueError, "must be a single byte"):
1233+
self.blob[0] = b"multiple"
1234+
with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"):
1235+
del self.blob[0]
1236+
with self.assertRaisesRegex(IndexError, "Blob index out of range"):
1237+
self.blob[1000] = b"a"
1238+
1239+
def test_blob_set_slice_error(self):
1240+
with self.assertRaisesRegex(IndexError, "wrong size"):
1241+
self.blob[5:10] = b"a"
1242+
with self.assertRaisesRegex(IndexError, "wrong size"):
1243+
self.blob[5:10] = b"a" * 1000
1244+
with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"):
1245+
del self.blob[5:10]
1246+
with self.assertRaisesRegex(ValueError, "step cannot be zero"):
1247+
self.blob[5:10:0] = b"12345"
1248+
with self.assertRaises(BufferError):
1249+
self.blob[5:10] = memoryview(b"abcde")[::2]
1250+
11651251
def test_blob_sequence_not_supported(self):
1166-
with self.assertRaises(TypeError):
1252+
with self.assertRaisesRegex(TypeError, "unsupported operand"):
11671253
self.blob + self.blob
1168-
with self.assertRaises(TypeError):
1254+
with self.assertRaisesRegex(TypeError, "unsupported operand"):
11691255
self.blob * 5
1170-
with self.assertRaises(TypeError):
1256+
with self.assertRaisesRegex(TypeError, "is not iterable"):
11711257
b"a" in self.blob
11721258

11731259
def test_blob_context_manager(self):
@@ -1209,6 +1295,14 @@ def test_blob_closed(self):
12091295
blob.__enter__()
12101296
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
12111297
blob.__exit__(None, None, None)
1298+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1299+
len(blob)
1300+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1301+
blob[0]
1302+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1303+
blob[0:1]
1304+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1305+
blob[0] = b""
12121306

12131307
def test_blob_closed_db_read(self):
12141308
with memory_database() as cx:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :term:`mapping` support to :class:`sqlite3.Blob`. Patch by Aviv Palivoda
2+
and Erlend E. Aasland.

Modules/_sqlite/blob.c

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,186 @@ blob_exit_impl(pysqlite_Blob *self, PyObject *type, PyObject *val,
347347
Py_RETURN_FALSE;
348348
}
349349

350+
static Py_ssize_t
351+
blob_length(pysqlite_Blob *self)
352+
{
353+
if (!check_blob(self)) {
354+
return -1;
355+
}
356+
return sqlite3_blob_bytes(self->blob);
357+
};
358+
359+
static int
360+
get_subscript_index(pysqlite_Blob *self, PyObject *item)
361+
{
362+
Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);
363+
if (i == -1 && PyErr_Occurred()) {
364+
return -1;
365+
}
366+
int blob_len = sqlite3_blob_bytes(self->blob);
367+
if (i < 0) {
368+
i += blob_len;
369+
}
370+
if (i < 0 || i >= blob_len) {
371+
PyErr_SetString(PyExc_IndexError, "Blob index out of range");
372+
return -1;
373+
}
374+
return i;
375+
}
376+
377+
static PyObject *
378+
subscript_index(pysqlite_Blob *self, PyObject *item)
379+
{
380+
int i = get_subscript_index(self, item);
381+
if (i < 0) {
382+
return NULL;
383+
}
384+
return inner_read(self, 1, i);
385+
}
386+
387+
static int
388+
get_slice_info(pysqlite_Blob *self, PyObject *item, Py_ssize_t *start,
389+
Py_ssize_t *stop, Py_ssize_t *step, Py_ssize_t *slicelen)
390+
{
391+
if (PySlice_Unpack(item, start, stop, step) < 0) {
392+
return -1;
393+
}
394+
int len = sqlite3_blob_bytes(self->blob);
395+
*slicelen = PySlice_AdjustIndices(len, start, stop, *step);
396+
return 0;
397+
}
398+
399+
static PyObject *
400+
subscript_slice(pysqlite_Blob *self, PyObject *item)
401+
{
402+
Py_ssize_t start, stop, step, len;
403+
if (get_slice_info(self, item, &start, &stop, &step, &len) < 0) {
404+
return NULL;
405+
}
406+
407+
if (step == 1) {
408+
return inner_read(self, len, start);
409+
}
410+
PyObject *blob = inner_read(self, stop - start, start);
411+
if (blob == NULL) {
412+
return NULL;
413+
}
414+
PyObject *result = PyBytes_FromStringAndSize(NULL, len);
415+
if (result != NULL) {
416+
char *blob_buf = PyBytes_AS_STRING(blob);
417+
char *res_buf = PyBytes_AS_STRING(result);
418+
for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
419+
res_buf[i] = blob_buf[j];
420+
}
421+
Py_DECREF(blob);
422+
}
423+
return result;
424+
}
425+
426+
static PyObject *
427+
blob_subscript(pysqlite_Blob *self, PyObject *item)
428+
{
429+
if (!check_blob(self)) {
430+
return NULL;
431+
}
432+
433+
if (PyIndex_Check(item)) {
434+
return subscript_index(self, item);
435+
}
436+
if (PySlice_Check(item)) {
437+
return subscript_slice(self, item);
438+
}
439+
440+
PyErr_SetString(PyExc_TypeError, "Blob indices must be integers");
441+
return NULL;
442+
}
443+
444+
static int
445+
ass_subscript_index(pysqlite_Blob *self, PyObject *item, PyObject *value)
446+
{
447+
if (value == NULL) {
448+
PyErr_SetString(PyExc_TypeError,
449+
"Blob doesn't support item deletion");
450+
return -1;
451+
}
452+
if (!PyBytes_Check(value) || PyBytes_Size(value) != 1) {
453+
PyErr_SetString(PyExc_ValueError,
454+
"Blob assignment must be a single byte");
455+
return -1;
456+
}
457+
458+
int i = get_subscript_index(self, item);
459+
if (i < 0) {
460+
return -1;
461+
}
462+
const char *buf = PyBytes_AS_STRING(value);
463+
return inner_write(self, buf, 1, i);
464+
}
465+
466+
static int
467+
ass_subscript_slice(pysqlite_Blob *self, PyObject *item, PyObject *value)
468+
{
469+
if (value == NULL) {
470+
PyErr_SetString(PyExc_TypeError,
471+
"Blob doesn't support slice deletion");
472+
return -1;
473+
}
474+
475+
Py_ssize_t start, stop, step, len;
476+
if (get_slice_info(self, item, &start, &stop, &step, &len) < 0) {
477+
return -1;
478+
}
479+
480+
if (len == 0) {
481+
return 0;
482+
}
483+
484+
Py_buffer vbuf;
485+
if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0) {
486+
return -1;
487+
}
488+
489+
int rc = -1;
490+
if (vbuf.len != len) {
491+
PyErr_SetString(PyExc_IndexError,
492+
"Blob slice assignment is wrong size");
493+
}
494+
else if (step == 1) {
495+
rc = inner_write(self, vbuf.buf, len, start);
496+
}
497+
else {
498+
PyObject *blob_bytes = inner_read(self, stop - start, start);
499+
if (blob_bytes != NULL) {
500+
char *blob_buf = PyBytes_AS_STRING(blob_bytes);
501+
for (Py_ssize_t i = 0, j = 0; i < len; i++, j += step) {
502+
blob_buf[j] = ((char *)vbuf.buf)[i];
503+
}
504+
rc = inner_write(self, blob_buf, stop - start, start);
505+
Py_DECREF(blob_bytes);
506+
}
507+
}
508+
PyBuffer_Release(&vbuf);
509+
return rc;
510+
}
511+
512+
static int
513+
blob_ass_subscript(pysqlite_Blob *self, PyObject *item, PyObject *value)
514+
{
515+
if (!check_blob(self)) {
516+
return -1;
517+
}
518+
519+
if (PyIndex_Check(item)) {
520+
return ass_subscript_index(self, item, value);
521+
}
522+
if (PySlice_Check(item)) {
523+
return ass_subscript_slice(self, item, value);
524+
}
525+
526+
PyErr_SetString(PyExc_TypeError, "Blob indices must be integers");
527+
return -1;
528+
}
529+
350530

351531
static PyMethodDef blob_methods[] = {
352532
BLOB_CLOSE_METHODDEF
@@ -370,6 +550,11 @@ static PyType_Slot blob_slots[] = {
370550
{Py_tp_clear, blob_clear},
371551
{Py_tp_methods, blob_methods},
372552
{Py_tp_members, blob_members},
553+
554+
// Mapping protocol
555+
{Py_mp_length, blob_length},
556+
{Py_mp_subscript, blob_subscript},
557+
{Py_mp_ass_subscript, blob_ass_subscript},
373558
{0, NULL},
374559
};
375560

0 commit comments

Comments
 (0)