Skip to content

Commit 05179f5

Browse files
committed
Define the PythonBytes type.
This type encapsulates the Python byte string object. This requires a new data type, mostly because there is no natural "bucket of bytes" type in Swift without adding a Foundation dependency, which this module currently does not have. Instead, we can define a little wrapper that makes it as easy as possible to convert from existing types to the Python bytes object, and back again.
1 parent 7b89a25 commit 05179f5

3 files changed

Lines changed: 138 additions & 0 deletions

File tree

PythonKit/Python.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,3 +1430,90 @@ extension PythonObject : ExpressibleByArrayLiteral, ExpressibleByDictionaryLiter
14301430
self.init(Dictionary(elements, uniquingKeysWith: { lhs, _ in lhs }))
14311431
}
14321432
}
1433+
1434+
public struct PythonBytes : PythonConvertible, ConvertibleFromPython, Hashable {
1435+
public private(set) var pythonObject: PythonObject
1436+
1437+
public init?(_ pythonObject: PythonObject) {
1438+
// We try to get the string/size pointers out. If it works, hooray, this is a bytes
1439+
// otherwise it isn't.
1440+
let pyObject = pythonObject.ownedPyObject
1441+
defer { Py_DecRef(pyObject) }
1442+
1443+
var length = 0
1444+
var buffer: UnsafeMutablePointer<CChar>? = nil
1445+
1446+
switch PyBytes_AsStringAndSize(pyObject, &buffer, &length) {
1447+
case 0:
1448+
self.pythonObject = pythonObject
1449+
default:
1450+
return nil
1451+
}
1452+
}
1453+
1454+
@inlinable
1455+
public init<Bytes: Sequence>(_ bytes: Bytes) where Bytes.Element == UInt8 {
1456+
let possibleSelf = bytes.withContiguousStorageIfAvailable { storagePtr in
1457+
PythonBytes.fromBytePointer(storagePtr)
1458+
}
1459+
if let actualSelf = possibleSelf {
1460+
self = actualSelf
1461+
} else {
1462+
let temporaryBuffer = Array(bytes)
1463+
self = temporaryBuffer.withUnsafeBufferPointer {
1464+
PythonBytes.fromBytePointer($0)
1465+
}
1466+
}
1467+
}
1468+
1469+
@inlinable
1470+
public init<Bytes: Sequence>(_ bytes: Bytes) where Bytes.Element == Int8 {
1471+
let possibleSelf = bytes.withContiguousStorageIfAvailable { storagePtr in
1472+
PythonBytes.fromBytePointer(storagePtr)
1473+
}
1474+
if let actualSelf = possibleSelf {
1475+
self = actualSelf
1476+
} else {
1477+
let temporaryBuffer = Array(bytes)
1478+
self = temporaryBuffer.withUnsafeBufferPointer {
1479+
PythonBytes.fromBytePointer($0)
1480+
}
1481+
}
1482+
}
1483+
1484+
private init(bytesObject: PythonObject) {
1485+
self.pythonObject = bytesObject
1486+
}
1487+
1488+
@usableFromInline
1489+
static func fromBytePointer(_ bytes: UnsafeBufferPointer<UInt8>) -> PythonBytes {
1490+
bytes.withMemoryRebound(to: Int8.self) { reboundPtr in
1491+
PythonBytes.fromBytePointer(reboundPtr)
1492+
}
1493+
}
1494+
1495+
@usableFromInline
1496+
static func fromBytePointer(_ bytes: UnsafeBufferPointer<Int8>) -> PythonBytes {
1497+
let v = PyBytes_FromStringAndSize(bytes.baseAddress, bytes.count)!
1498+
return PythonBytes(bytesObject: PythonObject(consuming: v))
1499+
}
1500+
1501+
public func withUnsafeBytes<ReturnValue>(
1502+
_ callback: (UnsafeRawBufferPointer) throws -> ReturnValue
1503+
) rethrows -> ReturnValue {
1504+
let pyObject = self.pythonObject.ownedPyObject
1505+
defer { Py_DecRef(pyObject) }
1506+
1507+
var length = 0
1508+
var buffer: UnsafeMutablePointer<CChar>? = nil
1509+
1510+
switch PyBytes_AsStringAndSize(pyObject, &buffer, &length) {
1511+
case 0:
1512+
let buffer = UnsafeRawBufferPointer(start: buffer, count: length)
1513+
return try callback(buffer)
1514+
default:
1515+
try! throwPythonErrorIfPresent()
1516+
fatalError("No result or error getting interior buffer for bytes \(self)")
1517+
}
1518+
}
1519+
}

PythonKit/PythonLibrary+Symbols.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,20 @@ let PyString_FromStringAndSize: @convention(c) (
185185
name: "PyUnicode_DecodeUTF8",
186186
legacyName: "PyString_FromStringAndSize")
187187

188+
let PyBytes_FromStringAndSize: @convention(c) (
189+
PyCCharPointer?, Int) -> (PyObjectPointer?) =
190+
PythonLibrary.loadSymbol(
191+
name: "PyBytes_FromStringAndSize",
192+
legacyName: "PyString_FromStringAndSize")
193+
194+
let PyBytes_AsStringAndSize: @convention(c) (
195+
PyObjectPointer,
196+
UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
197+
UnsafeMutablePointer<Int>?) -> CInt =
198+
PythonLibrary.loadSymbol(
199+
name: "PyBytes_AsStringAndSize",
200+
legacyName: "PyString_AsStringAndSize")
201+
188202
let _Py_ZeroStruct: PyObjectPointer =
189203
PythonLibrary.loadSymbol(name: "_Py_ZeroStruct")
190204

Tests/PythonKitTests/PythonRuntimeTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,41 @@ class PythonRuntimeTests: XCTestCase {
279279
_ = Bool.init(b)
280280
}
281281
}
282+
283+
func testPythonBytes() {
284+
let bytes = PythonBytes([UInt8(1), UInt8(2), UInt8(3), UInt8(4)])
285+
bytes.withUnsafeBytes {
286+
XCTAssertEqual(Array($0), [1, 2, 3, 4])
287+
}
288+
}
289+
290+
func testPythonBytesInt8() {
291+
let bytes = PythonBytes([Int8(1), Int8(2), Int8(3), Int8(4)])
292+
bytes.withUnsafeBytes {
293+
XCTAssertEqual(Array($0), [1, 2, 3, 4])
294+
}
295+
}
296+
297+
func testPythonBytesNonContiguousSequence() {
298+
let bytes = PythonBytes(CollectionOfOne(UInt8(1)))
299+
bytes.withUnsafeBytes {
300+
XCTAssertEqual(Array($0), [1])
301+
}
302+
}
303+
304+
func testPythonBytesNonContiguousSequenceInt8() {
305+
let bytes = PythonBytes(CollectionOfOne(Int8(1)))
306+
bytes.withUnsafeBytes {
307+
XCTAssertEqual(Array($0), [1])
308+
}
309+
}
310+
311+
func testBytesConversion() {
312+
let bytes = PythonBytes(CollectionOfOne(UInt8(1)))
313+
let otherBytes = PythonBytes(bytes.pythonObject)
314+
otherBytes?.withUnsafeBytes {
315+
XCTAssertEqual(Array($0), [1])
316+
}
317+
XCTAssertEqual(bytes, otherBytes)
318+
}
282319
}

0 commit comments

Comments
 (0)