@@ -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.
707731private 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
718741public 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 {
11491172private typealias PythonBinaryOp =
11501173 ( OwnedPyObjectPointer ? , OwnedPyObjectPointer ? ) -> OwnedPyObjectPointer ?
11511174private typealias PythonUnaryOp =
1152- ( OwnedPyObjectPointer ? ) -> OwnedPyObjectPointer ?
1175+ ( OwnedPyObjectPointer ? ) -> OwnedPyObjectPointer ?
11531176
11541177private func performBinaryOp(
11551178 _ op: PythonBinaryOp , lhs: PythonObject , rhs: PythonObject ) -> PythonObject {
@@ -1409,7 +1432,7 @@ extension PythonObject : Sequence {
14091432}
14101433
14111434extension 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///
15471570final 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
15571623public 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
15751656extension 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
16001693fileprivate 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
16931842extension PythonInstanceMethod : PythonConvertible {
0 commit comments