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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy