Skip to content

Commit e56b970

Browse files
authored
Merge pull request pvieito#53 from philipturner/philipturner/patch-9
Add support for `**kwargs`
2 parents 8de2a3f + 76b154b commit e56b970

File tree

3 files changed

+354
-62
lines changed

3 files changed

+354
-62
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,31 +1568,89 @@ 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
public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) {
15621628
function = PyFunction { argumentsAsTuple in
15631629
return try fn(argumentsAsTuple[0])
15641630
}
15651631
}
1566-
1567-
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead
1632+
1633+
/// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead.
15681634
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
15691635
function = PyFunction { argumentsAsTuple in
15701636
return try fn(argumentsAsTuple.map { $0 })
15711637
}
15721638
}
1639+
1640+
/// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python.
1641+
/// `**kwargs` must preserve order from Python 3.6 onward, similarly to
1642+
/// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be
1643+
/// mutated, so the next best solution is to use `[KeyValuePairs.Element]`.
1644+
public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) {
1645+
function = PyFunction { argumentsAsTuple, keywordArgumentsAsDictionary in
1646+
var kwargs: [(String, PythonObject)] = []
1647+
for keyAndValue in keywordArgumentsAsDictionary.items() {
1648+
let (key, value) = keyAndValue.tuple2
1649+
kwargs.append((String(key)!, value))
1650+
}
1651+
return try fn(argumentsAsTuple.map { $0 }, kwargs)
1652+
}
1653+
}
15731654
}
15741655

15751656
extension PythonFunction : PythonConvertible {
@@ -1581,14 +1662,26 @@ extension PythonFunction : PythonConvertible {
15811662
fatalError("PythonFunction only supports Python 3.1 and above.")
15821663
}
15831664

1584-
let funcPointer = Unmanaged.passRetained(function).toOpaque()
1585-
let capsulePointer = PyCapsule_New(funcPointer, nil, { capsulePointer in
1665+
let destructor: @convention(c) (PyObjectPointer?) -> Void = { capsulePointer in
15861666
let funcPointer = PyCapsule_GetPointer(capsulePointer, nil)
15871667
Unmanaged<PyFunction>.fromOpaque(funcPointer).release()
1588-
})
1668+
}
1669+
let funcPointer = Unmanaged.passRetained(function).toOpaque()
1670+
let capsulePointer = PyCapsule_New(
1671+
funcPointer,
1672+
nil,
1673+
unsafeBitCast(destructor, to: OpaquePointer.self)
1674+
)
15891675

1676+
var methodDefinition: UnsafeMutablePointer<PyMethodDef>
1677+
switch function.callingConvention {
1678+
case .varArgs:
1679+
methodDefinition = PythonFunction.sharedMethodDefinition
1680+
case .varArgsWithKeywords:
1681+
methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition
1682+
}
15901683
let pyFuncPointer = PyCFunction_NewEx(
1591-
PythonFunction.sharedMethodDefinition,
1684+
methodDefinition,
15921685
capsulePointer,
15931686
nil
15941687
)
@@ -1598,28 +1691,54 @@ extension PythonFunction : PythonConvertible {
15981691
}
15991692

16001693
fileprivate extension PythonFunction {
1601-
static let sharedFunctionName: UnsafePointer<Int8> = {
1694+
static let sharedMethodDefinition: UnsafeMutablePointer<PyMethodDef> = {
16021695
let name: StaticString = "pythonkit_swift_function"
16031696
// `utf8Start` is a property of StaticString, thus, it has a stable pointer.
1604-
return UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self)
1605-
}()
1697+
let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self)
1698+
1699+
let methodImplementationPointer = unsafeBitCast(
1700+
PythonFunction.sharedMethodImplementation, to: OpaquePointer.self)
16061701

1607-
static let sharedMethodDefinition: UnsafeMutablePointer<PyMethodDef> = {
16081702
/// The standard calling convention. See Python C API docs
1609-
let METH_VARARGS = 1 as Int32
1703+
let METH_VARARGS = 0x0001 as Int32
16101704

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

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

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

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

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

16701815
/// Combination of METH_xxx flags, which mostly describe the args expected by the C func
16711816
var ml_flags: Int32
@@ -1688,6 +1833,10 @@ public struct PythonInstanceMethod {
16881833
public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) {
16891834
function = PythonFunction(fn)
16901835
}
1836+
1837+
public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) {
1838+
function = PythonFunction(fn)
1839+
}
16911840
}
16921841

16931842
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)