Skip to content

Commit dee644f

Browse files
authored
Merge branch 'master' into ergonomic-args
2 parents b0b05d3 + e56b970 commit dee644f

File tree

3 files changed

+347
-55
lines changed

3 files changed

+347
-55
lines changed

PythonKit/Python.swift

Lines changed: 184 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,21 @@ public struct ThrowingPythonObject {
322322
public func dynamicallyCall(
323323
withKeywordArguments args:
324324
KeyValuePairs<String, PythonConvertible> = [:]) throws -> PythonObject {
325+
return try _dynamicallyCall(args)
326+
}
327+
328+
/// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal.
329+
/// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it.
330+
@discardableResult
331+
public func dynamicallyCall(
332+
withKeywordArguments args:
333+
[(key: String, value: PythonConvertible)] = []) throws -> PythonObject {
334+
return try _dynamicallyCall(args)
335+
}
336+
337+
/// Implementation of `dynamicallyCall(withKeywordArguments)`.
338+
private func _dynamicallyCall<T : Collection>(_ args: T) throws -> PythonObject
339+
where T.Element == (key: String, value: PythonConvertible) {
325340
try throwPythonErrorIfPresent()
326341

327342
// An array containing positional arguments.
@@ -615,6 +630,15 @@ public extension PythonObject {
615630
KeyValuePairs<String, PythonConvertible> = [:]) -> PythonObject {
616631
return try! throwing.dynamicallyCall(withKeywordArguments: args)
617632
}
633+
634+
/// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal.
635+
/// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it.
636+
@discardableResult
637+
func dynamicallyCall(
638+
withKeywordArguments args:
639+
[(key: String, value: PythonConvertible)] = []) -> PythonObject {
640+
return try! throwing.dynamicallyCall(withKeywordArguments: args)
641+
}
618642
}
619643

620644
//===----------------------------------------------------------------------===//
@@ -705,14 +729,13 @@ public struct PythonInterface {
705729

706730
// Create a Python tuple object with the specified elements.
707731
private func pyTuple<T : Collection>(_ vals: T) -> OwnedPyObjectPointer
708-
where T.Element : PythonConvertible {
709-
710-
let tuple = PyTuple_New(vals.count)!
711-
for (index, element) in vals.enumerated() {
712-
// `PyTuple_SetItem` steals the reference of the object stored.
713-
PyTuple_SetItem(tuple, index, element.ownedPyObject)
714-
}
715-
return tuple
732+
where T.Element : PythonConvertible {
733+
let tuple = PyTuple_New(vals.count)!
734+
for (index, element) in vals.enumerated() {
735+
// `PyTuple_SetItem` steals the reference of the object stored.
736+
PyTuple_SetItem(tuple, index, element.ownedPyObject)
737+
}
738+
return tuple
716739
}
717740

718741
public extension PythonObject {
@@ -723,13 +746,13 @@ public extension PythonObject {
723746
}
724747

725748
init<T : Collection>(tupleContentsOf elements: T)
726-
where T.Element == PythonConvertible {
727-
self.init(consuming: pyTuple(elements.map { $0.pythonObject }))
749+
where T.Element == PythonConvertible {
750+
self.init(consuming: pyTuple(elements.map { $0.pythonObject }))
728751
}
729752

730753
init<T : Collection>(tupleContentsOf elements: T)
731-
where T.Element : PythonConvertible {
732-
self.init(consuming: pyTuple(elements))
754+
where T.Element : PythonConvertible {
755+
self.init(consuming: pyTuple(elements))
733756
}
734757
}
735758

@@ -1149,7 +1172,7 @@ where Bound : ConvertibleFromPython {
11491172
private typealias PythonBinaryOp =
11501173
(OwnedPyObjectPointer?, OwnedPyObjectPointer?) -> OwnedPyObjectPointer?
11511174
private typealias PythonUnaryOp =
1152-
(OwnedPyObjectPointer?) -> OwnedPyObjectPointer?
1175+
(OwnedPyObjectPointer?) -> OwnedPyObjectPointer?
11531176

11541177
private func performBinaryOp(
11551178
_ op: PythonBinaryOp, lhs: PythonObject, rhs: PythonObject) -> PythonObject {
@@ -1409,7 +1432,7 @@ extension PythonObject : Sequence {
14091432
}
14101433

14111434
extension PythonObject {
1412-
public var count: Int {
1435+
public var count: Int {
14131436
checking.count!
14141437
}
14151438
}
@@ -1545,32 +1568,90 @@ public struct PythonBytes : PythonConvertible, ConvertibleFromPython, Hashable {
15451568
/// Python.map(PythonFunction { x in x * 2 }, [10, 12, 14]) // [20, 24, 28]
15461569
///
15471570
final class PyFunction {
1548-
private var callSwiftFunction: (_ argumentsTuple: PythonObject) throws -> PythonConvertible
1549-
init(_ callSwiftFunction: @escaping (_ argumentsTuple: PythonObject) throws -> PythonConvertible) {
1550-
self.callSwiftFunction = callSwiftFunction
1571+
enum CallingConvention {
1572+
case varArgs
1573+
case varArgsWithKeywords
1574+
}
1575+
1576+
/// Allows `PyFunction` to store Python functions with more than one possible calling convention
1577+
var callingConvention: CallingConvention
1578+
1579+
/// `arguments` is a Python tuple.
1580+
typealias VarArgsFunction = (
1581+
_ arguments: PythonObject) throws -> PythonConvertible
1582+
1583+
/// `arguments` is a Python tuple.
1584+
/// `keywordArguments` is an OrderedDict in Python 3.6 and later, or a dict otherwise.
1585+
typealias VarArgsWithKeywordsFunction = (
1586+
_ arguments: PythonObject,
1587+
_ keywordArguments: PythonObject) throws -> PythonConvertible
1588+
1589+
/// Has the same memory layout as any other function with the Swift calling convention
1590+
private typealias Storage = () throws -> PythonConvertible
1591+
1592+
/// Stores all function pointers in the same stored property. `callAsFunction` casts this into the desired type.
1593+
private var callSwiftFunction: Storage
1594+
1595+
init(_ callSwiftFunction: @escaping VarArgsFunction) {
1596+
self.callingConvention = .varArgs
1597+
self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self)
1598+
}
1599+
1600+
init(_ callSwiftFunction: @escaping VarArgsWithKeywordsFunction) {
1601+
self.callingConvention = .varArgsWithKeywords
1602+
self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self)
1603+
}
1604+
1605+
private func checkConvention(_ calledConvention: CallingConvention) {
1606+
precondition(callingConvention == calledConvention,
1607+
"Called PyFunction with convention \(calledConvention), but expected \(callingConvention)")
15511608
}
1609+
15521610
func callAsFunction(_ argumentsTuple: PythonObject) throws -> PythonConvertible {
1553-
try callSwiftFunction(argumentsTuple)
1611+
checkConvention(.varArgs)
1612+
let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsFunction.self)
1613+
return try callSwiftFunction(argumentsTuple)
1614+
}
1615+
1616+
func callAsFunction(_ argumentsTuple: PythonObject, _ keywordArguments: PythonObject) throws -> PythonConvertible {
1617+
checkConvention(.varArgsWithKeywords)
1618+
let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsWithKeywordsFunction.self)
1619+
return try callSwiftFunction(argumentsTuple, keywordArguments)
15541620
}
15551621
}
15561622

15571623
public struct PythonFunction {
15581624
/// Called directly by the Python C API
15591625
private var function: PyFunction
1560-
1626+
15611627
@_disfavoredOverload
15621628
public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) {
15631629
function = PyFunction { argumentsAsTuple in
15641630
return try fn(argumentsAsTuple[0])
15651631
}
15661632
}
1567-
1568-
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead
1633+
1634+
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead.
15691635
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
15701636
function = PyFunction { argumentsAsTuple in
15711637
return try fn(argumentsAsTuple.map { $0 })
15721638
}
15731639
}
1640+
1641+
/// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python.
1642+
/// `**kwargs` must preserve order from Python 3.6 onward, similarly to
1643+
/// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be
1644+
/// mutated, so the next best solution is to use `[KeyValuePairs.Element]`.
1645+
public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) {
1646+
function = PyFunction { argumentsAsTuple, keywordArgumentsAsDictionary in
1647+
var kwargs: [(String, PythonObject)] = []
1648+
for keyAndValue in keywordArgumentsAsDictionary.items() {
1649+
let (key, value) = keyAndValue.tuple2
1650+
kwargs.append((String(key)!, value))
1651+
}
1652+
return try fn(argumentsAsTuple.map { $0 }, kwargs)
1653+
}
1654+
}
15741655
}
15751656

15761657
extension PythonFunction : PythonConvertible {
@@ -1582,14 +1663,26 @@ extension PythonFunction : PythonConvertible {
15821663
fatalError("PythonFunction only supports Python 3.1 and above.")
15831664
}
15841665

1585-
let funcPointer = Unmanaged.passRetained(function).toOpaque()
1586-
let capsulePointer = PyCapsule_New(funcPointer, nil, { capsulePointer in
1666+
let destructor: @convention(c) (PyObjectPointer?) -> Void = { capsulePointer in
15871667
let funcPointer = PyCapsule_GetPointer(capsulePointer, nil)
15881668
Unmanaged<PyFunction>.fromOpaque(funcPointer).release()
1589-
})
1669+
}
1670+
let funcPointer = Unmanaged.passRetained(function).toOpaque()
1671+
let capsulePointer = PyCapsule_New(
1672+
funcPointer,
1673+
nil,
1674+
unsafeBitCast(destructor, to: OpaquePointer.self)
1675+
)
15901676

1677+
var methodDefinition: UnsafeMutablePointer<PyMethodDef>
1678+
switch function.callingConvention {
1679+
case .varArgs:
1680+
methodDefinition = PythonFunction.sharedMethodDefinition
1681+
case .varArgsWithKeywords:
1682+
methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition
1683+
}
15911684
let pyFuncPointer = PyCFunction_NewEx(
1592-
PythonFunction.sharedMethodDefinition,
1685+
methodDefinition,
15931686
capsulePointer,
15941687
nil
15951688
)
@@ -1599,28 +1692,54 @@ extension PythonFunction : PythonConvertible {
15991692
}
16001693

16011694
fileprivate extension PythonFunction {
1602-
static let sharedFunctionName: UnsafePointer<Int8> = {
1695+
static let sharedMethodDefinition: UnsafeMutablePointer<PyMethodDef> = {
16031696
let name: StaticString = "pythonkit_swift_function"
16041697
// `utf8Start` is a property of StaticString, thus, it has a stable pointer.
1605-
return UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self)
1606-
}()
1698+
let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self)
1699+
1700+
let methodImplementationPointer = unsafeBitCast(
1701+
PythonFunction.sharedMethodImplementation, to: OpaquePointer.self)
16071702

1608-
static let sharedMethodDefinition: UnsafeMutablePointer<PyMethodDef> = {
16091703
/// The standard calling convention. See Python C API docs
1610-
let METH_VARARGS = 1 as Int32
1704+
let METH_VARARGS = 0x0001 as Int32
16111705

16121706
let pointer = UnsafeMutablePointer<PyMethodDef>.allocate(capacity: 1)
16131707
pointer.pointee = PyMethodDef(
1614-
ml_name: PythonFunction.sharedFunctionName,
1615-
ml_meth: PythonFunction.sharedMethodImplementation,
1708+
ml_name: namePointer,
1709+
ml_meth: methodImplementationPointer,
16161710
ml_flags: METH_VARARGS,
16171711
ml_doc: nil
16181712
)
16191713

16201714
return pointer
16211715
}()
1716+
1717+
static let sharedMethodWithKeywordsDefinition: UnsafeMutablePointer<PyMethodDef> = {
1718+
let name: StaticString = "pythonkit_swift_function_with_keywords"
1719+
// `utf8Start` is a property of StaticString, thus, it has a stable pointer.
1720+
let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self)
1721+
1722+
let methodImplementationPointer = unsafeBitCast(
1723+
PythonFunction.sharedMethodWithKeywordsImplementation, to: OpaquePointer.self)
1724+
1725+
/// A combination of flags that supports `**kwargs`. See Python C API docs
1726+
let METH_VARARGS = 0x0001 as Int32
1727+
let METH_KEYWORDS = 0x0002 as Int32
1728+
1729+
let pointer = UnsafeMutablePointer<PyMethodDef>.allocate(capacity: 1)
1730+
pointer.pointee = PyMethodDef(
1731+
ml_name: namePointer,
1732+
ml_meth: methodImplementationPointer,
1733+
ml_flags: METH_VARARGS | METH_KEYWORDS,
1734+
ml_doc: nil
1735+
)
1736+
1737+
return pointer
1738+
}()
16221739

1623-
private static let sharedMethodImplementation: @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? = { context, argumentsPointer in
1740+
private static let sharedMethodImplementation: @convention(c) (
1741+
PyObjectPointer?, PyObjectPointer?
1742+
) -> PyObjectPointer? = { context, argumentsPointer in
16241743
guard let argumentsPointer = argumentsPointer, let capsulePointer = context else {
16251744
return nil
16261745
}
@@ -1636,6 +1755,31 @@ fileprivate extension PythonFunction {
16361755
return nil // This must only be `nil` if an exception has been set
16371756
}
16381757
}
1758+
1759+
private static let sharedMethodWithKeywordsImplementation: @convention(c) (
1760+
PyObjectPointer?, PyObjectPointer?, PyObjectPointer?
1761+
) -> PyObjectPointer? = { context, argumentsPointer, keywordArgumentsPointer in
1762+
guard let argumentsPointer = argumentsPointer, let capsulePointer = context else {
1763+
return nil
1764+
}
1765+
1766+
let funcPointer = PyCapsule_GetPointer(capsulePointer, nil)
1767+
let function = Unmanaged<PyFunction>.fromOpaque(funcPointer).takeUnretainedValue()
1768+
1769+
do {
1770+
let argumentsAsTuple = PythonObject(consuming: argumentsPointer)
1771+
var keywordArgumentsAsDictionary: PythonObject
1772+
if let keywordArgumentsPointer = keywordArgumentsPointer {
1773+
keywordArgumentsAsDictionary = PythonObject(consuming: keywordArgumentsPointer)
1774+
} else {
1775+
keywordArgumentsAsDictionary = [:]
1776+
}
1777+
return try function(argumentsAsTuple, keywordArgumentsAsDictionary).ownedPyObject
1778+
} catch {
1779+
PythonFunction.setPythonError(swiftError: error)
1780+
return nil // This must only be `nil` if an exception has been set
1781+
}
1782+
}
16391783

16401784
private static func setPythonError(swiftError: Error) {
16411785
if let pythonObject = swiftError as? PythonObject {
@@ -1665,8 +1809,9 @@ struct PyMethodDef {
16651809
/// The name of the built-in function/method
16661810
var ml_name: UnsafePointer<Int8>
16671811

1668-
/// The C function that implements it
1669-
var ml_meth: @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer?
1812+
/// The C function that implements it.
1813+
/// Since this accepts multiple function signatures, the Swift type must be opaque here.
1814+
var ml_meth: OpaquePointer
16701815

16711816
/// Combination of METH_xxx flags, which mostly describe the args expected by the C func
16721817
var ml_flags: Int32
@@ -1690,6 +1835,10 @@ public struct PythonInstanceMethod {
16901835
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
16911836
function = PythonFunction(fn)
16921837
}
1838+
1839+
public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) {
1840+
function = PythonFunction(fn)
1841+
}
16931842
}
16941843

16951844
extension PythonInstanceMethod : PythonConvertible {

PythonKit/PythonLibrary+Symbols.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,13 @@ let PyCFunction_NewEx: @convention(c) (PyMethodDefPointer, UnsafeMutableRawPoint
6363
let PyInstanceMethod_New: @convention(c) (PyObjectPointer) -> PyObjectPointer =
6464
PythonLibrary.loadSymbol(name: "PyInstanceMethod_New")
6565

66+
/// The last argument would ideally be of type `@convention(c) (PyObjectPointer?) -> Void`.
67+
/// Due to SR-15871 and the source-breaking nature of potential workarounds, the
68+
/// static typing was removed. The caller must now manually cast a closure to
69+
/// `OpaquePointer` before passing it into `PyCapsule_New`.
6670
let PyCapsule_New: @convention(c) (
6771
UnsafeMutableRawPointer, UnsafePointer<CChar>?,
68-
@convention(c) @escaping (PyObjectPointer?) -> Void) -> PyObjectPointer =
72+
OpaquePointer) -> PyObjectPointer =
6973
PythonLibrary.loadSymbol(name: "PyCapsule_New")
7074

7175
let PyCapsule_GetPointer: @convention(c) (PyObjectPointer?, UnsafePointer<CChar>?) -> UnsafeMutableRawPointer =

0 commit comments

Comments
 (0)