import XCTest import PythonKit class PythonFunctionTests: XCTestCase { private var canUsePythonFunction: Bool { let versionMajor = Python.versionInfo.major let versionMinor = Python.versionInfo.minor return (versionMajor == 3 && versionMinor >= 1) || versionMajor > 3 } func testPythonFunction() { guard canUsePythonFunction else { return } let pythonAdd = PythonFunction { args in let lhs = args[0] let rhs = args[1] return lhs + rhs }.pythonObject let pythonSum = pythonAdd(2, 3) XCTAssertNotNil(Double(pythonSum)) XCTAssertEqual(pythonSum, 5) // Test function with keyword arguments // Since there is no alternative function signature, `args` and `kwargs` // can be used without manually stating their type. This differs from // the behavior when there are no keywords. let pythonSelect = PythonFunction { args, kwargs in // NOTE: This may fail on Python versions before 3.6 because they do // not preserve order of keyword arguments XCTAssertEqual(args[0], true) XCTAssertEqual(kwargs[0].key, "y") XCTAssertEqual(kwargs[0].value, 2) XCTAssertEqual(kwargs[1].key, "x") XCTAssertEqual(kwargs[1].value, 3) let conditional = Bool(args[0])! let xIndex = kwargs.firstIndex(where: { $0.key == "x" })! let yIndex = kwargs.firstIndex(where: { $0.key == "y" })! return kwargs[conditional ? xIndex : yIndex].value }.pythonObject let pythonSelectOutput = pythonSelect(true, y: 2, x: 3) XCTAssertEqual(pythonSelectOutput, 3) } // From https://www.geeksforgeeks.org/create-classes-dynamically-in-python func testPythonClassConstruction() { guard canUsePythonFunction else { return } let constructor = PythonInstanceMethod { args in let `self` = args[0] `self`.constructor_arg = args[1] return Python.None } // Instead of calling `print`, use this to test what would be output. var printOutput: String? // Example of function using an alternative syntax for `args`. let displayMethod = PythonInstanceMethod { (args: [PythonObject]) in // let `self` = args[0] printOutput = String(args[1]) return Python.None } let classMethodOriginal = PythonInstanceMethod { args in // let cls = args[0] printOutput = String(args[1]) return Python.None } // Did not explicitly convert `constructor` or `displayMethod` to // PythonObject. This is intentional, as the `PythonClass` initializer // should take any `PythonConvertible` and not just `PythonObject`. let classMethod = Python.classmethod(classMethodOriginal.pythonObject) let Geeks = PythonClass("Geeks", members: [ // Constructor "__init__": constructor, // Data members "string_attribute": "Geeks 4 geeks!", "int_attribute": 1706256, // Member functions "func_arg": displayMethod, "class_func": classMethod, ]).pythonObject let obj = Geeks("constructor argument") XCTAssertEqual(obj.constructor_arg, "constructor argument") XCTAssertEqual(obj.string_attribute, "Geeks 4 geeks!") XCTAssertEqual(obj.int_attribute, 1706256) obj.func_arg("Geeks for Geeks") XCTAssertEqual(printOutput, "Geeks for Geeks") Geeks.class_func("Class Dynamically Created!") XCTAssertEqual(printOutput, "Class Dynamically Created!") } // Previously, there was a build error where passing a simple // `PythonClass.Members` literal made the literal's type ambiguous. It was // confused with `[String: PythonObject]`. The solution was adding a // `@_disfavoredOverload` attribute to the more specific initializer. func testPythonClassInitializer() { guard canUsePythonFunction else { return } let MyClass = PythonClass( "MyClass", superclasses: [Python.object], members: [ "memberName": "memberValue", ] ).pythonObject let memberValue = MyClass().memberName XCTAssertEqual(String(memberValue), "memberValue") } func testPythonClassInheritance() { guard canUsePythonFunction else { return } var helloOutput: String? var helloWorldOutput: String? // Declare subclasses of `Python.Exception` let HelloException = PythonClass( "HelloException", superclasses: [Python.Exception], members: [ "str_prefix": "HelloException-prefix ", "__init__": PythonInstanceMethod { args in let `self` = args[0] let message = "hello \(args[1])" helloOutput = String(message) // Conventional `super` syntax does not work; use this instead. Python.Exception.__init__(`self`, message) return Python.None }, // Example of function using the `self` convention instead of `args`. "__str__": PythonInstanceMethod { (`self`: PythonObject) in return `self`.str_prefix + Python.repr(`self`) } ] ).pythonObject let HelloWorldException = PythonClass( "HelloWorldException", superclasses: [HelloException], members: [ "str_prefix": "HelloWorldException-prefix ", "__init__": PythonInstanceMethod { args in let `self` = args[0] let message = "world \(args[1])" helloWorldOutput = String(message) `self`.int_param = args[2] // Conventional `super` syntax does not work; use this instead. HelloException.__init__(`self`, message) return Python.None }, // Example of function using the `self` convention instead of `args`. "custom_method": PythonInstanceMethod { (`self`: PythonObject) in return `self`.int_param } ] ).pythonObject // Test that inheritance works as expected let error1 = HelloException("test 1") XCTAssertEqual(helloOutput, "hello test 1") XCTAssertEqual(Python.str(error1), "HelloException-prefix HelloException('hello test 1')") XCTAssertEqual(Python.repr(error1), "HelloException('hello test 1')") let error2 = HelloWorldException("test 1", 123) XCTAssertEqual(helloOutput, "hello world test 1") XCTAssertEqual(helloWorldOutput, "world test 1") XCTAssertEqual(Python.str(error2), "HelloWorldException-prefix HelloWorldException('hello world test 1')") XCTAssertEqual(Python.repr(error2), "HelloWorldException('hello world test 1')") XCTAssertEqual(error2.custom_method(), 123) XCTAssertNotEqual(error2.custom_method(), "123") // Test that subclasses behave like Python exceptions // Example of function with no named parameters, which can be stated // ergonomically using an underscore. The ignored input is a [PythonObject]. let _ = PythonFunction { _ in throw HelloWorldException("EXAMPLE ERROR MESSAGE", 2) }.pythonObject /* do { try testFunction.throwing.dynamicallyCall(withArguments: []) XCTFail("testFunction did not throw an error.") } catch PythonError.exception(let error, _) { guard let description = String(error) else { XCTFail("A string could not be created from a HelloWorldException.") return } XCTAssertTrue(description.contains("EXAMPLE ERROR MESSAGE")) XCTAssertTrue(description.contains("HelloWorldException")) } catch { XCTFail("Got error that was not a Python exception: \(error.localizedDescription)") } */ } // Tests the ability to dynamically construct an argument list with keywords // and instantiate a `PythonInstanceMethod` with keywords. func testPythonClassInheritanceWithKeywords() { guard canUsePythonFunction else { return } func getValue(key: String, kwargs: [(String, PythonObject)]) -> PythonObject { let index = kwargs.firstIndex(where: { $0.0 == key })! return kwargs[index].1 } // Base class has the following arguments: // __init__(): // - 1 unnamed argument // - param1 // - param2 // // test_method(): // - param1 // - param2 let BaseClass = PythonClass( "BaseClass", superclasses: [], members: [ "__init__": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.arg1 = args[1] `self`.param1 = getValue(key: "param1", kwargs: kwargs) `self`.param2 = getValue(key: "param2", kwargs: kwargs) return Python.None }, "test_method": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param1 += getValue(key: "param1", kwargs: kwargs) `self`.param2 += getValue(key: "param2", kwargs: kwargs) return Python.None } ] ).pythonObject // Derived class accepts the following arguments: // __init__(): // - param2 // - param3 // // test_method(): // - param1 // - param2 // - param3 let DerivedClass = PythonClass( "DerivedClass", superclasses: [], members: [ "__init__": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param3 = getValue(key: "param3", kwargs: kwargs) // Lists the arguments in an order different than they are // specified (self, param2, param3, param1, arg1). The // correct order is (self, arg1, param1, param2, param3). let newKeywordArguments = args.map { ("", $0) } + kwargs + [ ("param1", 1), ("", 0) ] BaseClass.__init__.dynamicallyCall( withKeywordArguments: newKeywordArguments) return Python.None }, "test_method": PythonInstanceMethod { args, kwargs in let `self` = args[0] `self`.param3 += getValue(key: "param3", kwargs: kwargs) BaseClass.test_method.dynamicallyCall( withKeywordArguments: args.map { ("", $0) } + kwargs) return Python.None } ] ).pythonObject let derivedInstance = DerivedClass(param2: 2, param3: 3) XCTAssertEqual(derivedInstance.arg1, 0) XCTAssertEqual(derivedInstance.param1, 1) XCTAssertEqual(derivedInstance.param2, 2) XCTAssertEqual(derivedInstance.checking.param3, 3) derivedInstance.test_method(param1: 1, param2: 2, param3: 3) XCTAssertEqual(derivedInstance.arg1, 0) XCTAssertEqual(derivedInstance.param1, 2) XCTAssertEqual(derivedInstance.param2, 4) XCTAssertEqual(derivedInstance.checking.param3, 6) // Validate that subclassing and instantiating the derived class does // not affect behavior of the parent class. let baseInstance = BaseClass(0, param1: 10, param2: 20) XCTAssertEqual(baseInstance.arg1, 0) XCTAssertEqual(baseInstance.param1, 10) XCTAssertEqual(baseInstance.param2, 20) XCTAssertEqual(baseInstance.checking.param3, nil) baseInstance.test_method(param1: 10, param2: 20) XCTAssertEqual(baseInstance.arg1, 0) XCTAssertEqual(baseInstance.param1, 20) XCTAssertEqual(baseInstance.param2, 40) XCTAssertEqual(baseInstance.checking.param3, nil) } }