Skip to content

Commit 29afb7d

Browse files
author
Erlend Egeberg Aasland
authored
gh-69093: Add indexing and slicing support to sqlite3.Blob (#91599)
Authored-by: Aviv Palivoda <palaviv@gmail.com> Co-authored-by: Erlend E. Aasland <erlend.aasland@innova.no>
1 parent 1317b70 commit 29afb7d

File tree

5 files changed

+349
-16
lines changed

5 files changed

+349
-16
lines changed

Doc/includes/sqlite3/blob.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
con = sqlite3.connect(":memory:")
44
con.execute("create table test(blob_col blob)")
5-
con.execute("insert into test(blob_col) values (zeroblob(10))")
5+
con.execute("insert into test(blob_col) values (zeroblob(13))")
66

77
# Write to our blob, using two write operations:
88
with con.blobopen("test", "blob_col", 1) as blob:
9-
blob.write(b"Hello")
10-
blob.write(b"World")
9+
blob.write(b"hello, ")
10+
blob.write(b"world.")
11+
# Modify the first and last bytes of our blob
12+
blob[0] = b"H"
13+
blob[-1] = b"!"
1114

1215
# Read the contents of our blob
1316
with con.blobopen("test", "blob_col", 1) as blob:
1417
greeting = blob.read()
1518

16-
print(greeting) # outputs "b'HelloWorld'"
19+
print(greeting) # outputs "b'Hello, world!'"

Doc/library/sqlite3.rst

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

10521052
.. class:: Blob
10531053

1054-
A :class:`Blob` instance is a :term:`file-like object` that can read and write
1055-
data in an SQLite :abbr:`BLOB (Binary Large OBject)`. Call ``len(blob)`` to
1056-
get the size (number of bytes) of the blob.
1054+
A :class:`Blob` instance is a :term:`file-like object`
1055+
that can read and write data in an SQLite :abbr:`BLOB (Binary Large OBject)`.
1056+
Call :func:`len(blob) <len>` to get the size (number of bytes) of the blob.
1057+
Use indices and :term:`slices <slice>` for direct access to the blob data.
10571058

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

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 134 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

@@ -1138,6 +1138,13 @@ def test_blob_write_error_length(self):
11381138
with self.assertRaisesRegex(ValueError, "data longer than blob"):
11391139
self.blob.write(b"a" * 1000)
11401140

1141+
self.blob.seek(0, SEEK_SET)
1142+
n = len(self.blob)
1143+
self.blob.write(b"a" * (n-1))
1144+
self.blob.write(b"a")
1145+
with self.assertRaisesRegex(ValueError, "data longer than blob"):
1146+
self.blob.write(b"a")
1147+
11411148
def test_blob_write_error_row_changed(self):
11421149
self.cx.execute("update test set b='aaaa' where rowid=1")
11431150
with self.assertRaises(sqlite.OperationalError):
@@ -1162,12 +1169,127 @@ def test_blob_open_error(self):
11621169
with self.assertRaisesRegex(sqlite.OperationalError, regex):
11631170
self.cx.blobopen(*args, **kwds)
11641171

1172+
def test_blob_length(self):
1173+
self.assertEqual(len(self.blob), 50)
1174+
1175+
def test_blob_get_item(self):
1176+
self.assertEqual(self.blob[5], b"b")
1177+
self.assertEqual(self.blob[6], b"l")
1178+
self.assertEqual(self.blob[7], b"o")
1179+
self.assertEqual(self.blob[8], b"b")
1180+
self.assertEqual(self.blob[-1], b"!")
1181+
1182+
def test_blob_set_item(self):
1183+
self.blob[0] = b"b"
1184+
expected = b"b" + self.data[1:]
1185+
actual = self.cx.execute("select b from test").fetchone()[0]
1186+
self.assertEqual(actual, expected)
1187+
1188+
def test_blob_set_item_with_offset(self):
1189+
self.blob.seek(0, SEEK_END)
1190+
self.assertEqual(self.blob.read(), b"") # verify that we're at EOB
1191+
self.blob[0] = b"T"
1192+
self.blob[-1] = b"."
1193+
self.blob.seek(0, SEEK_SET)
1194+
expected = b"This blob data string is exactly fifty bytes long."
1195+
self.assertEqual(self.blob.read(), expected)
1196+
1197+
def test_blob_set_buffer_object(self):
1198+
from array import array
1199+
self.blob[0] = memoryview(b"1")
1200+
self.assertEqual(self.blob[0], b"1")
1201+
1202+
self.blob[1] = bytearray(b"2")
1203+
self.assertEqual(self.blob[1], b"2")
1204+
1205+
self.blob[2] = array("b", [4])
1206+
self.assertEqual(self.blob[2], b"\x04")
1207+
1208+
self.blob[0:5] = memoryview(b"12345")
1209+
self.assertEqual(self.blob[0:5], b"12345")
1210+
1211+
self.blob[0:5] = bytearray(b"23456")
1212+
self.assertEqual(self.blob[0:5], b"23456")
1213+
1214+
self.blob[0:5] = array("b", [1, 2, 3, 4, 5])
1215+
self.assertEqual(self.blob[0:5], b"\x01\x02\x03\x04\x05")
1216+
1217+
def test_blob_set_item_negative_index(self):
1218+
self.blob[-1] = b"z"
1219+
self.assertEqual(self.blob[-1], b"z")
1220+
1221+
def test_blob_get_slice(self):
1222+
self.assertEqual(self.blob[5:14], b"blob data")
1223+
1224+
def test_blob_get_empty_slice(self):
1225+
self.assertEqual(self.blob[5:5], b"")
1226+
1227+
def test_blob_get_slice_negative_index(self):
1228+
self.assertEqual(self.blob[5:-5], self.data[5:-5])
1229+
1230+
def test_blob_get_slice_with_skip(self):
1231+
self.assertEqual(self.blob[0:10:2], b"ti lb")
1232+
1233+
def test_blob_set_slice(self):
1234+
self.blob[0:5] = b"12345"
1235+
expected = b"12345" + self.data[5:]
1236+
actual = self.cx.execute("select b from test").fetchone()[0]
1237+
self.assertEqual(actual, expected)
1238+
1239+
def test_blob_set_empty_slice(self):
1240+
self.blob[0:0] = b""
1241+
self.assertEqual(self.blob[:], self.data)
1242+
1243+
def test_blob_set_slice_with_skip(self):
1244+
self.blob[0:10:2] = b"12345"
1245+
actual = self.cx.execute("select b from test").fetchone()[0]
1246+
expected = b"1h2s3b4o5 " + self.data[10:]
1247+
self.assertEqual(actual, expected)
1248+
1249+
def test_blob_mapping_invalid_index_type(self):
1250+
msg = "indices must be integers"
1251+
with self.assertRaisesRegex(TypeError, msg):
1252+
self.blob[5:5.5]
1253+
with self.assertRaisesRegex(TypeError, msg):
1254+
self.blob[1.5]
1255+
with self.assertRaisesRegex(TypeError, msg):
1256+
self.blob["a"] = b"b"
1257+
1258+
def test_blob_get_item_error(self):
1259+
dataset = [len(self.blob), 105, -105]
1260+
for idx in dataset:
1261+
with self.subTest(idx=idx):
1262+
with self.assertRaisesRegex(IndexError, "index out of range"):
1263+
self.blob[idx]
1264+
with self.assertRaisesRegex(IndexError, "cannot fit 'int'"):
1265+
self.blob[ULLONG_MAX]
1266+
1267+
def test_blob_set_item_error(self):
1268+
with self.assertRaisesRegex(ValueError, "must be a single byte"):
1269+
self.blob[0] = b"multiple"
1270+
with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"):
1271+
del self.blob[0]
1272+
with self.assertRaisesRegex(IndexError, "Blob index out of range"):
1273+
self.blob[1000] = b"a"
1274+
1275+
def test_blob_set_slice_error(self):
1276+
with self.assertRaisesRegex(IndexError, "wrong size"):
1277+
self.blob[5:10] = b"a"
1278+
with self.assertRaisesRegex(IndexError, "wrong size"):
1279+
self.blob[5:10] = b"a" * 1000
1280+
with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"):
1281+
del self.blob[5:10]
1282+
with self.assertRaisesRegex(ValueError, "step cannot be zero"):
1283+
self.blob[5:10:0] = b"12345"
1284+
with self.assertRaises(BufferError):
1285+
self.blob[5:10] = memoryview(b"abcde")[::2]
1286+
11651287
def test_blob_sequence_not_supported(self):
1166-
with self.assertRaises(TypeError):
1288+
with self.assertRaisesRegex(TypeError, "unsupported operand"):
11671289
self.blob + self.blob
1168-
with self.assertRaises(TypeError):
1290+
with self.assertRaisesRegex(TypeError, "unsupported operand"):
11691291
self.blob * 5
1170-
with self.assertRaises(TypeError):
1292+
with self.assertRaisesRegex(TypeError, "is not iterable"):
11711293
b"a" in self.blob
11721294

11731295
def test_blob_context_manager(self):
@@ -1209,6 +1331,14 @@ def test_blob_closed(self):
12091331
blob.__enter__()
12101332
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
12111333
blob.__exit__(None, None, None)
1334+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1335+
len(blob)
1336+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1337+
blob[0]
1338+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1339+
blob[0:1]
1340+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
1341+
blob[0] = b""
12121342

12131343
def test_blob_closed_db_read(self):
12141344
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 indexing and slicing support to :class:`sqlite3.Blob`. Patch by Aviv Palivoda
2+
and Erlend E. Aasland.

0 commit comments

Comments
 (0)