diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e2b1bd2..e265347 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -11,37 +11,15 @@ jobs: os: - ubuntu-latest - macos-latest - - windows-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - - uses: actions/setup-python@v2 - with: - python-version: '2.7' - - name: Test (Python 2) - run: swift test --enable-test-discovery - if: runner.os != 'Windows' - env: - PYTHON_VERSION: 2 - PYTHON_LOADER_LOGGING: TRUE - - name: Test (Python 2 on Windows) - uses: MaxDesiatov/swift-windows-action@v1 - if: runner.os == 'Windows' - env: - PYTHON_VERSION: 2 - PYTHON_LOADER_LOGGING: TRUE - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Test (Python 3) + - name: Test run: swift test --enable-test-discovery if: runner.os != 'Windows' env: PYTHON_VERSION: 3 PYTHON_LOADER_LOGGING: TRUE - - name: Test (Python 3 on Windows) - uses: MaxDesiatov/swift-windows-action@v1 - if: runner.os == 'Windows' - env: - PYTHON_VERSION: 3 - PYTHON_LOADER_LOGGING: TRUE diff --git a/.gitignore b/.gitignore index 9d88ee1..3d50865 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # Temporary Items *.tmp *.tmp.* +temp # Virtual Environments /venv*/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8b46202 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Agents + +This file provides development guidelines for AI agents working with this Swift Package Manager project. + +## Development + +### Code Patterns + +- Use appropriate logging frameworks instead of print statements. +- Follow protocol-oriented design patterns. +- **CRITICAL: If they are listed on `Package.swift`, always check `FoundationKit`, `LoggerKit`, and other core frameworks before implementing basic functionality.** These frameworks contain extensive extensions and utilities that avoid code duplication (typically you can find them in the project parent directory). + +#### Core Frameworks Functionality Examples + +- **NSError**: `NSError(description:, recoverySuggestion:)` convenience initializer. +- **NSAppleScript**: `execute()` method with proper Swift error handling. +- **ProcessInfo**: `launchExtensionsPaneInSystemSettings()`, `launchPrivacyAndSecurityPaneInSystemSettings()`. +- **URL**: `open(withAppBundleIdentifier:)` for cross-platform URL opening. +- **Process**: Enhanced execution utilities with output capture. +- **UserDefaults**: `@UserDefaults.Wrapper` property wrapper for cleaner app settings. + +### Building and Testing + +- **Always verify changes work** by building or testing before considering task complete. +- Changes are not complete until successfully built and verified. + +--- + +**IMPORTANT**: This is a generic development guide for AI agents shared and available on multiple projects, so avoid adding project-specific information here. Refer to `README.md` file if available and the source code for project-specific details. diff --git a/PythonKit/NumpyConversion.swift b/PythonKit/NumpyConversion.swift index fc73a33..e0c81a2 100644 --- a/PythonKit/NumpyConversion.swift +++ b/PythonKit/NumpyConversion.swift @@ -110,18 +110,18 @@ where Element : NumpyScalarCompatible { guard Element.numpyScalarTypes.contains(numpyArray.dtype) else { return nil } - + // Only 1-D `ndarray` instances can be converted to `Array`. let pyShape = numpyArray.__array_interface__["shape"] guard let shape = Array(pyShape) else { return nil } guard shape.count == 1 else { return nil } - + // Make sure that the array is contiguous in memory. This does a copy if // the array is not already contiguous in memory. let contiguousNumpyArray = np.ascontiguousarray(numpyArray) - + guard let ptrVal = UInt(contiguousNumpyArray.__array_interface__["data"].tuple2.0) else { return nil @@ -137,7 +137,7 @@ where Element : NumpyScalarCompatible { self.init(repeating: dummyPointer.move(), count: scalarCount) dummyPointer.deallocate() withUnsafeMutableBufferPointer { buffPtr in - buffPtr.baseAddress!.assign(from: ptr, count: scalarCount) + buffPtr.baseAddress!.update(from: ptr, count: scalarCount) } } } diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 7a7a05b..35d2fcb 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -39,7 +39,7 @@ typealias OwnedPyObjectPointer = PyObjectPointer @usableFromInline @_fixed_layout final class PyReference { private var pointer: OwnedPyObjectPointer - + // This `PyReference`, once deleted, will make no delta change to the // python object's reference count. It will however, retain the reference for // the lifespan of this object. @@ -47,21 +47,21 @@ final class PyReference { self.pointer = pointer Py_IncRef(pointer) } - + // This `PyReference` adopts the +1 reference and will decrement it in the // future. init(consuming pointer: PyObjectPointer) { self.pointer = pointer } - + deinit { Py_DecRef(pointer) } - + var borrowedPyObject: PyObjectPointer { return pointer } - + var ownedPyObject: OwnedPyObjectPointer { Py_IncRef(pointer) return pointer @@ -90,26 +90,26 @@ final class PyReference { public struct PythonObject { /// The underlying `PyReference`. fileprivate var reference: PyReference - + @usableFromInline init(_ pointer: PyReference) { reference = pointer } - + /// Creates a new instance and a new reference. init(_ pointer: OwnedPyObjectPointer) { reference = PyReference(pointer) } - + /// Creates a new instance consuming the specified `PyObject` pointer. init(consuming pointer: PyObjectPointer) { reference = PyReference(consuming: pointer) } - + fileprivate var borrowedPyObject: PyObjectPointer { return reference.borrowedPyObject } - + fileprivate var ownedPyObject: OwnedPyObjectPointer { return reference.ownedPyObject } @@ -165,7 +165,7 @@ fileprivate extension PythonConvertible { var borrowedPyObject: PyObjectPointer { return pythonObject.borrowedPyObject } - + var ownedPyObject: OwnedPyObjectPointer { return pythonObject.ownedPyObject } @@ -189,7 +189,7 @@ extension PythonObject : PythonConvertible, ConvertibleFromPython { public init(_ object: PythonObject) { self.init(consuming: object.ownedPyObject) } - + public var pythonObject: PythonObject { return self } } @@ -210,7 +210,7 @@ public extension PythonObject { public enum PythonError : Error, Equatable { /// A Python runtime exception, produced by calling a Python function. case exception(PythonObject, traceback: PythonObject?) - + /// A failed call on a `PythonObject`. /// Reasons for failure include: /// - A non-callable Python object was called. @@ -218,7 +218,7 @@ public enum PythonError : Error, Equatable { /// object. /// - An invalid keyword argument was specified. case invalidCall(PythonObject) - + /// A module import error. case invalidModule(String) } @@ -248,14 +248,14 @@ extension PythonError : CustomStringConvertible { // active. private func throwPythonErrorIfPresent() throws { if PyErr_Occurred() == nil { return } - + var type: PyObjectPointer? var value: PyObjectPointer? var traceback: PyObjectPointer? - + // Fetch the exception and clear the exception state. PyErr_Fetch(&type, &value, &traceback) - + // The value for the exception may not be set but the type always should be. let resultObject = PythonObject(consuming: value ?? type!) let tracebackObject = traceback.flatMap { PythonObject(consuming: $0) } @@ -271,11 +271,11 @@ private func throwPythonErrorIfPresent() throws { /// `dynamicallyCall` until further discussion/design. public struct ThrowingPythonObject { private var base: PythonObject - + fileprivate init(_ base: PythonObject) { self.base = base } - + /// Call `self` with the specified positional arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -285,7 +285,7 @@ public struct ThrowingPythonObject { withArguments args: PythonConvertible...) throws -> PythonObject { return try dynamicallyCall(withArguments: args) } - + /// Call `self` with the specified positional arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -294,18 +294,18 @@ public struct ThrowingPythonObject { public func dynamicallyCall( withArguments args: [PythonConvertible] = []) throws -> PythonObject { try throwPythonErrorIfPresent() - + // Positional arguments are passed as a tuple of objects. let argTuple = pyTuple(args.map { $0.pythonObject }) defer { Py_DecRef(argTuple) } - + // Python calls always return a non-null object when successful. If the // Python function produces the equivalent of C `void`, it returns the // `None` object. A `null` result of `PyObjectCall` happens when there is an // error, like `self` not being a Python callable. let selfObject = base.ownedPyObject defer { Py_DecRef(selfObject) } - + guard let result = PyObject_CallObject(selfObject, argTuple) else { // If a Python exception was thrown, throw a corresponding Swift error. try throwPythonErrorIfPresent() @@ -313,7 +313,7 @@ public struct ThrowingPythonObject { } return PythonObject(consuming: result) } - + /// Call `self` with the specified arguments. /// If the call fails for some reason, `PythonError.invalidCall` is thrown. /// - Precondition: `self` must be a Python callable. @@ -324,7 +324,7 @@ public struct ThrowingPythonObject { KeyValuePairs = [:]) throws -> PythonObject { return try _dynamicallyCall(args) } - + /// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal. /// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it. @discardableResult @@ -333,17 +333,17 @@ public struct ThrowingPythonObject { [(key: String, value: PythonConvertible)] = []) throws -> PythonObject { return try _dynamicallyCall(args) } - + /// Implementation of `dynamicallyCall(withKeywordArguments)`. private func _dynamicallyCall(_ args: T) throws -> PythonObject where T.Element == (key: String, value: PythonConvertible) { try throwPythonErrorIfPresent() - + // An array containing positional arguments. var positionalArgs: [PythonObject] = [] // A dictionary object for storing keyword arguments, if any exist. var kwdictObject: OwnedPyObjectPointer? = nil - + for (key, value) in args { if key.isEmpty { positionalArgs.append(value.pythonObject) @@ -360,20 +360,20 @@ public struct ThrowingPythonObject { Py_DecRef(k) Py_DecRef(v) } - + defer { Py_DecRef(kwdictObject) } // Py_DecRef is `nil` safe. - + // Positional arguments are passed as a tuple of objects. let argTuple = pyTuple(positionalArgs) defer { Py_DecRef(argTuple) } - + // Python calls always return a non-null object when successful. If the // Python function produces the equivalent of C `void`, it returns the // `None` object. A `null` result of `PyObjectCall` happens when there is an // error, like `self` not being a Python callable. let selfObject = base.ownedPyObject defer { Py_DecRef(selfObject) } - + guard let result = PyObject_Call(selfObject, argTuple, kwdictObject) else { // If a Python exception was thrown, throw a corresponding Swift error. try throwPythonErrorIfPresent() @@ -381,7 +381,7 @@ public struct ThrowingPythonObject { } return PythonObject(consuming: result) } - + /// Converts to a 2-tuple, if possible. public var tuple2: (PythonObject, PythonObject)? { let ct = base.checking @@ -390,7 +390,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1) } - + /// Converts to a 3-tuple, if possible. public var tuple3: (PythonObject, PythonObject, PythonObject)? { let ct = base.checking @@ -399,7 +399,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1, elt2) } - + /// Converts to a 4-tuple, if possible. public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { let ct = base.checking @@ -409,7 +409,7 @@ public struct ThrowingPythonObject { } return (elt0, elt1, elt2, elt3) } - + public var count: Int? { base.checking.count } @@ -434,11 +434,11 @@ public extension PythonObject { public struct CheckingPythonObject { /// The underlying `PythonObject`. private var base: PythonObject - + fileprivate init(_ base: PythonObject) { self.base = base } - + public subscript(dynamicMember name: String) -> PythonObject? { get { let selfObject = base.ownedPyObject @@ -451,7 +451,7 @@ public struct CheckingPythonObject { return PythonObject(consuming: result) } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -463,7 +463,7 @@ public struct CheckingPythonObject { Py_DecRef(keyObject) Py_DecRef(selfObject) } - + // `PyObject_GetItem` returns +1 reference. if let result = PyObject_GetItem(selfObject, keyObject) { return PythonObject(consuming: result) @@ -478,7 +478,7 @@ public struct CheckingPythonObject { Py_DecRef(keyObject) Py_DecRef(selfObject) } - + if let newValue = newValue { let newValueObject = newValue.ownedPyObject PyObject_SetItem(selfObject, keyObject, newValueObject) @@ -489,7 +489,7 @@ public struct CheckingPythonObject { } } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -501,7 +501,7 @@ public struct CheckingPythonObject { self[key] = newValue } } - + /// Converts to a 2-tuple, if possible. public var tuple2: (PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1] else { @@ -509,7 +509,7 @@ public struct CheckingPythonObject { } return (elt0, elt1) } - + /// Converts to a 3-tuple, if possible. public var tuple3: (PythonObject, PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1], let elt2 = self[2] else { @@ -517,7 +517,7 @@ public struct CheckingPythonObject { } return (elt0, elt1, elt2) } - + /// Converts to a 4-tuple, if possible. public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { guard let elt0 = self[0], let elt1 = self[1], @@ -526,7 +526,7 @@ public struct CheckingPythonObject { } return (elt0, elt1, elt2, elt3) } - + public var count: Int? { Int(Python.len(base)) } @@ -559,7 +559,7 @@ public extension PythonObject { defer { Py_DecRef(selfObject) } let valueObject = newValue.ownedPyObject defer { Py_DecRef(valueObject) } - + if PyObject_SetAttrString(selfObject, memberName, valueObject) == -1 { try! throwPythonErrorIfPresent() fatalError(""" @@ -569,7 +569,7 @@ public extension PythonObject { } } } - + /// Access the element corresponding to the specified `PythonConvertible` /// values representing a key. /// - Note: This is equivalent to `object[key]` in Python. @@ -587,7 +587,7 @@ public extension PythonObject { checking[key] = newValue } } - + /// Converts to a 2-tuple. var tuple2: (PythonObject, PythonObject) { guard let result = checking.tuple2 else { @@ -595,7 +595,7 @@ public extension PythonObject { } return result } - + /// Converts to a 3-tuple. var tuple3: (PythonObject, PythonObject, PythonObject) { guard let result = checking.tuple3 else { @@ -603,7 +603,7 @@ public extension PythonObject { } return result } - + /// Converts to a 4-tuple. var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject) { guard let result = checking.tuple4 else { @@ -611,7 +611,7 @@ public extension PythonObject { } return result } - + /// Call `self` with the specified positional arguments. /// - Precondition: `self` must be a Python callable. /// - Parameter args: Positional arguments for the Python callable. @@ -620,7 +620,7 @@ public extension PythonObject { withArguments args: [PythonConvertible] = []) -> PythonObject { return try! throwing.dynamicallyCall(withArguments: args) } - + /// Call `self` with the specified arguments. /// - Precondition: `self` must be a Python callable. /// - Parameter args: Positional or keyword arguments for the Python callable. @@ -630,7 +630,7 @@ public extension PythonObject { KeyValuePairs = [:]) -> PythonObject { return try! throwing.dynamicallyCall(withKeywordArguments: args) } - + /// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal. /// This function must be called explicitly on a `PythonObject` because `@dynamicCallable` does not recognize it. @discardableResult @@ -673,27 +673,28 @@ public let Python = PythonInterface() public struct PythonInterface { /// A dictionary of the Python builtins. public let builtins: PythonObject - + init() { Py_Initialize() // Initialize Python builtins = PythonObject(PyEval_GetBuiltins()) - + // Runtime Fixes: PyRun_SimpleString(""" import sys import os - - # Some Python modules expect to have at least one argument in `sys.argv`. + + # Some Python modules expect to have at least one argument in `sys.argv`: sys.argv = [""] # Some Python modules require `sys.executable` to return the path # to the Python interpreter executable. In Darwin, Python 3 returns the - # main process executable path instead. + # main process executable path instead: if sys.version_info.major == 3 and sys.platform == "darwin": - sys.executable = os.path.join(sys.exec_prefix, "bin", "python3") + executable_name = "python{}.{}".format(sys.version_info.major, sys.version_info.minor) + sys.executable = os.path.join(sys.exec_prefix, "bin", executable_name) """) } - + public func attemptImport(_ name: String) throws -> PythonObject { guard let module = PyImport_ImportModule(name) else { try throwPythonErrorIfPresent() @@ -701,26 +702,35 @@ public struct PythonInterface { } return PythonObject(consuming: module) } - + public func `import`(_ name: String) -> PythonObject { return try! attemptImport(name) } - + public subscript(dynamicMember name: String) -> PythonObject { return builtins[name] } - + // The Python runtime version. // Equivalent to `sys.version` in Python. public var version: PythonObject { return self.import("sys").version } - + // The Python runtime version information. // Equivalent to `sys.version_info` in Python. public var versionInfo: PythonObject { return self.import("sys").version_info } + + /// Emulates a Python `with` statement. + /// - Parameter object: A context manager object. + /// - Parameter body: A closure to call on the result of `object.__enter__()`. + public func with(_ object: PythonObject, _ body: (PythonObject) throws -> Void) rethrows { + let yieldValue = object.__enter__() + try body(yieldValue) + yieldValue.__exit__() + } } //===----------------------------------------------------------------------===// @@ -744,12 +754,12 @@ public extension PythonObject { init(tupleOf elements: PythonConvertible...) { self.init(tupleContentsOf: elements) } - + init(tupleContentsOf elements: T) where T.Element == PythonConvertible { self.init(consuming: pyTuple(elements.map { $0.pythonObject })) } - + init(tupleContentsOf elements: T) where T.Element : PythonConvertible { self.init(consuming: pyTuple(elements)) @@ -765,14 +775,14 @@ public extension PythonObject { private func isType(_ object: PythonObject, type: PyObjectPointer) -> Bool { let typePyRef = PythonObject(type) - + let result = Python.isinstance(object, typePyRef) - + // We cannot use the normal failable Bool initializer from `PythonObject` // here because would cause an infinite loop. let pyObject = result.ownedPyObject defer { Py_DecRef(pyObject) } - + // Anything not equal to `Py_ZeroStruct` is truthy. return pyObject != _Py_ZeroStruct } @@ -780,13 +790,13 @@ private func isType(_ object: PythonObject, extension Bool : PythonConvertible, ConvertibleFromPython { public init?(_ pythonObject: PythonObject) { guard isType(pythonObject, type: PyBool_Type) else { return nil } - + let pyObject = pythonObject.ownedPyObject defer { Py_DecRef(pyObject) } - + self = pyObject == _Py_TrueStruct } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyBool_FromLong(self ? 1 : 0)) @@ -797,14 +807,14 @@ extension String : PythonConvertible, ConvertibleFromPython { public init?(_ pythonObject: PythonObject) { let pyObject = pythonObject.ownedPyObject defer { Py_DecRef(pyObject) } - + guard let cString = PyString_AsString(pyObject) else { PyErr_Clear() return nil } self.init(cString: cString) } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. let v = utf8CString.withUnsafeBufferPointer { @@ -824,17 +834,17 @@ fileprivate extension PythonObject { ) -> T? { let pyObject = ownedPyObject defer { Py_DecRef(pyObject) } - + assert(PyErr_Occurred() == nil, "Python error occurred somewhere but wasn't handled") - + let value = converter(pyObject) guard value != errorValue || PyErr_Occurred() == nil else { PyErr_Clear() return nil } return value - + } } @@ -848,7 +858,7 @@ extension Int : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyInt_FromLong(self)) @@ -866,7 +876,7 @@ extension UInt : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyInt_FromSize_t(self)) @@ -883,7 +893,7 @@ extension Double : PythonConvertible, ConvertibleFromPython { } self = value } - + public var pythonObject: PythonObject { _ = Python // Ensure Python is initialized. return PythonObject(consuming: PyFloat_FromDouble(self)) @@ -902,7 +912,7 @@ extension Int8 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -913,7 +923,7 @@ extension Int16 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -924,7 +934,7 @@ extension Int32 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -935,7 +945,7 @@ extension Int64 : PythonConvertible, ConvertibleFromPython { guard let i = Int(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return Int(self).pythonObject } @@ -946,7 +956,7 @@ extension UInt8 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -957,7 +967,7 @@ extension UInt16 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -968,7 +978,7 @@ extension UInt32 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -979,7 +989,7 @@ extension UInt64 : PythonConvertible, ConvertibleFromPython { guard let i = UInt(pythonObject) else { return nil } self.init(i) } - + public var pythonObject: PythonObject { return UInt(self).pythonObject } @@ -992,7 +1002,7 @@ extension Float : PythonConvertible, ConvertibleFromPython { guard let v = Double(pythonObject) else { return nil } self.init(v) } - + public var pythonObject: PythonObject { return Double(self).pythonObject } @@ -1077,12 +1087,12 @@ extension Dictionary : ConvertibleFromPython where Key : ConvertibleFromPython, Value : ConvertibleFromPython { public init?(_ pythonDict: PythonObject) { self = [:] - + // Iterate over the Python dictionary, converting its keys and values to // Swift `Key` and `Value` pairs. var key, value: PyObjectPointer? var position: Int = 0 - + while PyDict_Next( pythonDict.borrowedPyObject, &position, &key, &value) != 0 { @@ -1194,7 +1204,7 @@ public extension PythonObject { static func + (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Add, lhs: lhs, rhs: rhs) } - + static func - (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Subtract, lhs: lhs, rhs: rhs) } @@ -1202,23 +1212,23 @@ public extension PythonObject { static func * (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_Multiply, lhs: lhs, rhs: rhs) } - + static func / (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return performBinaryOp(PyNumber_TrueDivide, lhs: lhs, rhs: rhs) } - + static func += (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceAdd, lhs: lhs, rhs: rhs) } - + static func -= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceSubtract, lhs: lhs, rhs: rhs) } - + static func *= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceMultiply, lhs: lhs, rhs: rhs) } - + static func /= (lhs: inout PythonObject, rhs: PythonObject) { lhs = performBinaryOp(PyNumber_InPlaceTrueDivide, lhs: lhs, rhs: rhs) } @@ -1258,15 +1268,15 @@ extension PythonObject : SignedNumeric { public init(exactly value: T) { self.init(Int(value)) } - + public typealias Magnitude = PythonObject - + public var magnitude: PythonObject { return self < 0 ? -self : self } - //override the default implementation of - prefix function - //from SignedNumeric (https://bugs.swift.org/browse/SR-13293) + // Override the default implementation of `-` prefix function + // from SignedNumeric (https://bugs.swift.org/browse/SR-13293). public static prefix func - (_ operand: Self) -> Self { return performUnaryOp(PyNumber_Negative, operand: operand) } @@ -1274,11 +1284,11 @@ extension PythonObject : SignedNumeric { extension PythonObject : Strideable { public typealias Stride = PythonObject - + public func distance(to other: PythonObject) -> Stride { return other - self } - + public func advanced(by stride: Stride) -> PythonObject { return self + stride } @@ -1308,7 +1318,7 @@ extension PythonObject : Equatable, Comparable { public static func == (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_EQ) } - + public static func != (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_NE) } @@ -1320,11 +1330,11 @@ extension PythonObject : Equatable, Comparable { public static func <= (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_LE) } - + public static func > (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_GT) } - + public static func >= (lhs: PythonObject, rhs: PythonObject) -> Bool { return lhs.compared(to: rhs, byOp: Py_GE) } @@ -1347,27 +1357,27 @@ public extension PythonObject { } return PythonObject(consuming: result) } - + static func == (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_EQ) } - + static func != (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_NE) } - + static func < (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_LT) } - + static func <= (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_LE) } - + static func > (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_GT) } - + static func >= (lhs: PythonObject, rhs: PythonObject) -> PythonObject { return lhs.compared(to: rhs, byOp: Py_GE) } @@ -1385,19 +1395,19 @@ extension PythonObject : Hashable { extension PythonObject : MutableCollection { public typealias Index = PythonObject public typealias Element = PythonObject - + public var startIndex: Index { return 0 } - + public var endIndex: Index { return Python.len(self) } - + public func index(after i: Index) -> Index { return i + PythonObject(1) } - + public subscript(index: PythonObject) -> PythonObject { get { return self[index as PythonConvertible] @@ -1411,7 +1421,7 @@ extension PythonObject : MutableCollection { extension PythonObject : Sequence { public struct Iterator : IteratorProtocol { fileprivate let pythonIterator: PythonObject - + public func next() -> PythonObject? { guard let result = PyIter_Next(self.pythonIterator.borrowedPyObject) else { try! throwPythonErrorIfPresent() @@ -1420,7 +1430,7 @@ extension PythonObject : Sequence { return PythonObject(consuming: result) } } - + public func makeIterator() -> Iterator { guard let result = PyObject_GetIter(borrowedPyObject) else { try! throwPythonErrorIfPresent() @@ -1463,8 +1473,36 @@ extension PythonObject : ExpressibleByArrayLiteral, ExpressibleByDictionaryLiter } public typealias Key = PythonObject public typealias Value = PythonObject + + // Preserves element order in the final Python object, unlike + // `Dictionary.pythonObject`. When keys are duplicated, throw the same + // runtime error as `Swift.Dictionary.init(dictionaryLiteral:)`. This + // differs from Python's key uniquing semantics, which silently override an + // existing key with the next one it encounters. public init(dictionaryLiteral elements: (PythonObject, PythonObject)...) { - self.init(Dictionary(elements, uniquingKeysWith: { lhs, _ in lhs })) + _ = Python // Ensure Python is initialized. + let dict = PyDict_New()! + for (key, value) in elements { + let k = key.ownedPyObject + let v = value.ownedPyObject + + // Use Python's native key checking instead of querying whether + // `elements` contains the key. Although this could theoretically + // produce different results, it produces the Python object we want. + switch PyDict_Contains(dict, k) { + case 0: + PyDict_SetItem(dict, k, v) + case 1: + fatalError("Dictionary literal contains duplicate keys") + default: + try! throwPythonErrorIfPresent() + fatalError("No result or error checking whether \(elements) contains \(key)") + } + + Py_DecRef(k) + Py_DecRef(v) + } + self.init(consuming: dict) } } @@ -1572,47 +1610,47 @@ final class PyFunction { case varArgs case varArgsWithKeywords } - + /// Allows `PyFunction` to store Python functions with more than one possible calling convention var callingConvention: CallingConvention - + /// `arguments` is a Python tuple. typealias VarArgsFunction = ( _ arguments: PythonObject) throws -> PythonConvertible - + /// `arguments` is a Python tuple. /// `keywordArguments` is an OrderedDict in Python 3.6 and later, or a dict otherwise. typealias VarArgsWithKeywordsFunction = ( _ arguments: PythonObject, _ keywordArguments: PythonObject) throws -> PythonConvertible - + /// Has the same memory layout as any other function with the Swift calling convention private typealias Storage = () throws -> PythonConvertible - + /// Stores all function pointers in the same stored property. `callAsFunction` casts this into the desired type. private var callSwiftFunction: Storage - + init(_ callSwiftFunction: @escaping VarArgsFunction) { self.callingConvention = .varArgs self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self) } - + init(_ callSwiftFunction: @escaping VarArgsWithKeywordsFunction) { self.callingConvention = .varArgsWithKeywords self.callSwiftFunction = unsafeBitCast(callSwiftFunction, to: Storage.self) } - + private func checkConvention(_ calledConvention: CallingConvention) { precondition(callingConvention == calledConvention, "Called PyFunction with convention \(calledConvention), but expected \(callingConvention)") } - + func callAsFunction(_ argumentsTuple: PythonObject) throws -> PythonConvertible { checkConvention(.varArgs) let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsFunction.self) return try callSwiftFunction(argumentsTuple) } - + func callAsFunction(_ argumentsTuple: PythonObject, _ keywordArguments: PythonObject) throws -> PythonConvertible { checkConvention(.varArgsWithKeywords) let callSwiftFunction = unsafeBitCast(self.callSwiftFunction, to: VarArgsWithKeywordsFunction.self) @@ -1623,21 +1661,21 @@ final class PyFunction { public struct PythonFunction { /// Called directly by the Python C API private var function: PyFunction - + @_disfavoredOverload public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in return try fn(argumentsAsTuple[0]) } } - + /// For cases where the Swift function should accept more (or less) than one parameter, accept an ordered array of all arguments instead. public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PyFunction { argumentsAsTuple in return try fn(argumentsAsTuple.map { $0 }) } } - + /// For cases where the Swift function should accept keyword arguments as `**kwargs` in Python. /// `**kwargs` must preserve order from Python 3.6 onward, similarly to /// Swift `KeyValuePairs` and unlike `Dictionary`. `KeyValuePairs` cannot be @@ -1713,7 +1751,7 @@ fileprivate extension PythonFunction { return pointer }() - + static let sharedMethodWithKeywordsDefinition: UnsafeMutablePointer = { let name: StaticString = "pythonkit_swift_function_with_keywords" // `utf8Start` is a property of StaticString, thus, it has a stable pointer. @@ -1755,7 +1793,7 @@ fileprivate extension PythonFunction { return nil // This must only be `nil` if an exception has been set } } - + private static let sharedMethodWithKeywordsImplementation: @convention(c) ( PyObjectPointer?, PyObjectPointer?, PyObjectPointer? ) -> PyObjectPointer? = { context, argumentsPointer, keywordArgumentsPointer in @@ -1826,16 +1864,16 @@ struct PyMethodDef { public struct PythonInstanceMethod { private var function: PythonFunction - + @_disfavoredOverload public init(_ fn: @escaping (PythonObject) throws -> PythonConvertible) { function = PythonFunction(fn) } - + public init(_ fn: @escaping ([PythonObject]) throws -> PythonConvertible) { function = PythonFunction(fn) } - + public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) { function = PythonFunction(fn) } @@ -1855,33 +1893,35 @@ extension PythonInstanceMethod : PythonConvertible { public struct PythonClass { private var typeObject: PythonObject - + public struct Members: ExpressibleByDictionaryLiteral { public typealias Key = String public typealias Value = PythonConvertible - + var dictionary: [String: PythonObject] - + public init(dictionaryLiteral elements: (Key, Value)...) { let castedElements = elements.map { (key, value) in (key, value.pythonObject) } - - dictionary = Dictionary(castedElements, uniquingKeysWith: { lhs, _ in lhs }) + + dictionary = Dictionary(castedElements, uniquingKeysWith: { _, _ in + fatalError("Dictionary literal contains duplicate keys") + }) } } public init(_ name: String, superclasses: [PythonObject] = [], members: Members = [:]) { self.init(name, superclasses: superclasses, members: members.dictionary) } - + @_disfavoredOverload public init(_ name: String, superclasses: [PythonObject] = [], members: [String: PythonObject] = [:]) { var trueSuperclasses = superclasses if !trueSuperclasses.contains(Python.object) { trueSuperclasses.append(Python.object) } - + let superclassesTuple = PythonObject(tupleContentsOf: trueSuperclasses) typeObject = Python.type(name, superclassesTuple, members.pythonObject) } diff --git a/PythonKit/PythonLibrary+Symbols.swift b/PythonKit/PythonLibrary+Symbols.swift index bd7ceb6..c4db681 100644 --- a/PythonKit/PythonLibrary+Symbols.swift +++ b/PythonKit/PythonLibrary+Symbols.swift @@ -15,7 +15,7 @@ //===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===// -// Required Python typealias and constants. +// Required Python typealiases and constants. //===----------------------------------------------------------------------===// @usableFromInline @@ -93,6 +93,10 @@ let PyErr_Fetch: @convention(c) ( let PyDict_New: @convention(c) () -> PyObjectPointer? = PythonLibrary.loadSymbol(name: "PyDict_New") +let PyDict_Contains: @convention(c) ( + PyObjectPointer?, PyObjectPointer?) -> Int32 = + PythonLibrary.loadSymbol(name: "PyDict_Contains") + let PyDict_SetItem: @convention(c) ( PyObjectPointer?, PyObjectPointer, PyObjectPointer) -> Void = PythonLibrary.loadSymbol(name: "PyDict_SetItem") diff --git a/PythonKit/PythonLibrary.swift b/PythonKit/PythonLibrary.swift index 42d9bcb..ab94a3f 100644 --- a/PythonKit/PythonLibrary.swift +++ b/PythonKit/PythonLibrary.swift @@ -18,6 +18,8 @@ import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) import CRT import WinSDK @@ -30,7 +32,7 @@ import WinSDK public struct PythonLibrary { public enum Error: Swift.Error, Equatable, CustomStringConvertible { case pythonLibraryNotFound - + public var description: String { switch self { case .pythonLibraryNotFound: @@ -41,25 +43,23 @@ public struct PythonLibrary { } } } - + private static let pythonInitializeSymbolName = "Py_Initialize" private static let pythonLegacySymbolName = "PyString_AsString" - - #if canImport(Darwin) + +#if canImport(Darwin) private static let defaultLibraryHandle = UnsafeMutableRawPointer(bitPattern: -2) // RTLD_DEFAULT - #elseif canImport(Glibc) +#else private static let defaultLibraryHandle: UnsafeMutableRawPointer? = nil // RTLD_DEFAULT - #elseif os(Windows) - private static let defaultLibraryHandle: UnsafeMutableRawPointer? = nil // Unsupported - #endif - +#endif + private static var isPythonLibraryLoaded = false private static var _pythonLibraryHandle: UnsafeMutableRawPointer? private static var pythonLibraryHandle: UnsafeMutableRawPointer? { try! PythonLibrary.loadLibrary() return self._pythonLibraryHandle } - + /// Tries to load the Python library, will throw an error if no compatible library is found. public static func loadLibrary() throws { guard !self.isPythonLibraryLoaded else { return } @@ -70,7 +70,7 @@ public struct PythonLibrary { self.isPythonLibraryLoaded = true self._pythonLibraryHandle = pythonLibraryHandle } - + private static let isLegacyPython: Bool = { let isLegacyPython = PythonLibrary.loadSymbol(PythonLibrary.pythonLibraryHandle, PythonLibrary.pythonLegacySymbolName) != nil if isLegacyPython { @@ -78,70 +78,70 @@ public struct PythonLibrary { } return isLegacyPython }() - + internal static func loadSymbol( name: String, legacyName: String? = nil, type: T.Type = T.self) -> T { - var name = name - if let legacyName = legacyName, self.isLegacyPython { - name = legacyName + var name = name + if let legacyName = legacyName, self.isLegacyPython { + name = legacyName + } + + log("Loading symbol '\(name)' from the Python library...") + return unsafeBitCast(self.loadSymbol(self.pythonLibraryHandle, name), to: type) } - - log("Loading symbol '\(name)' from the Python library...") - return unsafeBitCast(self.loadSymbol(self.pythonLibraryHandle, name), to: type) - } } // Methods of `PythonLibrary` required to load the Python library. extension PythonLibrary { private static let supportedMajorVersions: [Int] = [3, 2] private static let supportedMinorVersions: [Int] = Array(0...30).reversed() - + private static let libraryPathVersionCharacter: Character = ":" - - #if canImport(Darwin) + +#if canImport(Darwin) private static var libraryNames = ["Python.framework/Versions/:/Python"] private static var libraryPathExtensions = [""] private static var librarySearchPaths = ["", "/opt/homebrew/Frameworks/", "/usr/local/Frameworks/"] private static var libraryVersionSeparator = "." - #elseif os(Linux) +#elseif os(Linux) private static var libraryNames = ["libpython:", "libpython:m"] private static var libraryPathExtensions = [".so"] private static var librarySearchPaths = [""] private static var libraryVersionSeparator = "." - #elseif os(Windows) +#elseif os(Windows) private static var libraryNames = ["python:"] private static var libraryPathExtensions = [".dll"] private static var librarySearchPaths = [""] private static var libraryVersionSeparator = "" - #endif - +#endif + private static let libraryPaths: [String] = { var libraryPaths: [String] = [] for librarySearchPath in librarySearchPaths { for libraryName in libraryNames { for libraryPathExtension in libraryPathExtensions { let libraryPath = - librarySearchPath + libraryName + libraryPathExtension + librarySearchPath + libraryName + libraryPathExtension libraryPaths.append(libraryPath) } } } return libraryPaths }() - + private static func loadSymbol( _ libraryHandle: UnsafeMutableRawPointer?, _ name: String) -> UnsafeMutableRawPointer? { - #if canImport(Darwin) || canImport(Glibc) - return dlsym(libraryHandle, name) - #elseif os(Windows) - guard let libraryHandle = libraryHandle else { return nil } - let moduleHandle = libraryHandle - .assumingMemoryBound(to: HINSTANCE__.self) - let moduleSymbol = GetProcAddress(moduleHandle, name) - return unsafeBitCast(moduleSymbol, to: UnsafeMutableRawPointer?.self) - #endif - } - +#if os(Windows) + guard let libraryHandle = libraryHandle else { return nil } + let moduleHandle = libraryHandle + .assumingMemoryBound(to: HINSTANCE__.self) + let moduleSymbol = GetProcAddress(moduleHandle, name) + return unsafeBitCast(moduleSymbol, to: UnsafeMutableRawPointer?.self) +#else + return dlsym(libraryHandle, name) +#endif + } + private static func isPythonLibraryLoaded(at pythonLibraryHandle: UnsafeMutableRawPointer? = nil) -> Bool { let pythonLibraryHandle = pythonLibraryHandle ?? self.defaultLibraryHandle return self.loadSymbol(pythonLibraryHandle, self.pythonInitializeSymbolName) != nil @@ -160,7 +160,7 @@ extension PythonLibrary { } return pythonLibraryHandle } - + private static func findAndLoadExternalPythonLibrary() -> UnsafeMutableRawPointer? { for majorVersion in supportedMajorVersions { for minorVersion in supportedMinorVersions { @@ -168,7 +168,7 @@ extension PythonLibrary { let version = PythonVersion(major: majorVersion, minor: minorVersion) guard let pythonLibraryHandle = loadPythonLibrary( at: libraryPath, version: version) else { - continue + continue } return pythonLibraryHandle } @@ -176,37 +176,44 @@ extension PythonLibrary { } return nil } - + private static func loadPythonLibrary( at path: String, version: PythonVersion) -> UnsafeMutableRawPointer? { - let versionString = version.versionString - - if let requiredPythonVersion = Environment.version.value { - let requiredMajorVersion = Int(requiredPythonVersion) - if requiredPythonVersion != versionString, - requiredMajorVersion != version.major { - return nil + let versionString = version.versionString + + if let requiredPythonVersion = Environment.version.value { + let requiredMajorVersion = Int(requiredPythonVersion) + if requiredPythonVersion != versionString, + requiredMajorVersion != version.major { + return nil + } } + + let libraryVersionString = versionString + .split(separator: PythonVersion.versionSeparator) + .joined(separator: libraryVersionSeparator) + let path = path.split(separator: libraryPathVersionCharacter) + .joined(separator: libraryVersionString) + return self.loadPythonLibrary(at: path) } - - let libraryVersionString = versionString - .split(separator: PythonVersion.versionSeparator) - .joined(separator: libraryVersionSeparator) - let path = path.split(separator: libraryPathVersionCharacter) - .joined(separator: libraryVersionString) - return self.loadPythonLibrary(at: path) - } - + private static func loadPythonLibrary(at path: String) -> UnsafeMutableRawPointer? { self.log("Trying to load library at '\(path)'...") - #if canImport(Darwin) || canImport(Glibc) +#if os(Windows) + let pythonLibraryHandle = UnsafeMutableRawPointer(LoadLibraryA(path)) +#else // Must be RTLD_GLOBAL because subsequent .so files from the imported python // modules may depend on this .so file. let pythonLibraryHandle = dlopen(path, RTLD_LAZY | RTLD_GLOBAL) - #elseif os(Windows) - let pythonLibraryHandle = UnsafeMutableRawPointer(LoadLibraryA(path)) - #endif - + if pythonLibraryHandle == nil { + self.log("Failed to load library at '\(path)'.") + if let errorCString = dlerror() { + let errorString = String(cString: errorCString) + self.log("Reason for failure: \(errorString)") + } + } +#endif + if pythonLibraryHandle != nil { self.log("Library at '\(path)' was successfully loaded.") } @@ -222,7 +229,7 @@ extension PythonLibrary { has already been loaded. """) } - + /// Use the Python library with the specified version. /// - Parameters: /// - major: Major version or nil to use any Python version. @@ -232,7 +239,7 @@ extension PythonLibrary { let version = PythonVersion(major: major, minor: minor) PythonLibrary.Environment.version.set(version.versionString) } - + /// Use the Python library at the specified path. /// - Parameter path: Path of the Python library to load or nil to use the default search path. public static func useLibrary(at path: String?) { @@ -246,9 +253,9 @@ extension PythonLibrary { private struct PythonVersion { let major: Int? let minor: Int? - + static let versionSeparator: Character = "." - + init(major: Int?, minor: Int?) { precondition(!(major == nil && minor != nil), """ Error: The Python library minor version cannot be specified \ @@ -257,7 +264,7 @@ extension PythonLibrary { self.major = major self.minor = minor } - + var versionString: String { guard let major = major else { return "" } var versionString = String(major) @@ -274,28 +281,28 @@ extension PythonLibrary { private enum Environment: String { private static let keyPrefix = "PYTHON" private static let keySeparator = "_" - + case library = "LIBRARY" case version = "VERSION" case loaderLogging = "LOADER_LOGGING" - + var key: String { return Environment.keyPrefix + Environment.keySeparator + rawValue } - + var value: String? { guard let cString = getenv(key) else { return nil } let value = String(cString: cString) guard !value.isEmpty else { return nil } return value } - + func set(_ value: String) { - #if canImport(Darwin) || canImport(Glibc) - setenv(key, value, 1) - #elseif os(Windows) +#if os(Windows) _putenv_s(key, value) - #endif +#else + setenv(key, value, 1) +#endif } } } diff --git a/README.md b/README.md index fe7f3d4..83feb56 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ print("Python Encoding: \(sys.getdefaultencoding().upper())") Add the following dependency to your `Package.swift` manifest: ```swift -.package(url: "https://github.com/pvieito/PythonKit.git", .branch("master")), +.package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), ``` ## Environment Variables diff --git a/Tests/PythonKitTests/NumpyConversionTests.swift b/Tests/PythonKitTests/NumpyConversionTests.swift index 7f47946..4189470 100644 --- a/Tests/PythonKitTests/NumpyConversionTests.swift +++ b/Tests/PythonKitTests/NumpyConversionTests.swift @@ -3,25 +3,25 @@ import PythonKit class NumpyConversionTests: XCTestCase { static var numpyModule = try? Python.attemptImport("numpy") - + func testArrayConversion() { guard let np = NumpyConversionTests.numpyModule else { return } - + let numpyArrayEmpty = np.array([] as [Float], dtype: np.float32) XCTAssertEqual([], Array(numpy: numpyArrayEmpty)) - + let numpyArrayBool = np.array([true, false, false, true]) XCTAssertEqual([true, false, false, true], Array(numpy: numpyArrayBool)) - + let numpyArrayFloat = np.ones([6], dtype: np.float32) XCTAssertEqual(Array(repeating: 1, count: 6), Array(numpy: numpyArrayFloat)) - + let numpyArrayInt32 = np.array([-1, 4, 25, 2018], dtype: np.int32) XCTAssertEqual([-1, 4, 25, 2018], Array(numpy: numpyArrayInt32)) - + let numpyArray2D = np.ones([2, 3]) XCTAssertNil(Array(numpy: numpyArray2D)) - + let numpyArrayStrided = np.array([[1, 2], [1, 2]], dtype: np.int32)[ Python.slice(Python.None), 1] // Assert that the array has a stride, so that we're certainly testing a diff --git a/Tests/PythonKitTests/PythonFunctionTests.swift b/Tests/PythonKitTests/PythonFunctionTests.swift index bcb26a5..f11ad57 100644 --- a/Tests/PythonKitTests/PythonFunctionTests.swift +++ b/Tests/PythonKitTests/PythonFunctionTests.swift @@ -7,24 +7,24 @@ class PythonFunctionTests: XCTestCase { 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. @@ -36,76 +36,76 @@ class PythonFunctionTests: XCTestCase { 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 @@ -114,7 +114,7 @@ class PythonFunctionTests: XCTestCase { guard canUsePythonFunction else { return } - + let MyClass = PythonClass( "MyClass", superclasses: [Python.object], @@ -122,76 +122,76 @@ class PythonFunctionTests: XCTestCase { "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") @@ -199,15 +199,16 @@ class PythonFunctionTests: XCTestCase { 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 testFunction = PythonFunction { _ in + let _ = PythonFunction { _ in throw HelloWorldException("EXAMPLE ERROR MESSAGE", 2) }.pythonObject - + + /* do { try testFunction.throwing.dynamicallyCall(withArguments: []) XCTFail("testFunction did not throw an error.") @@ -216,26 +217,27 @@ class PythonFunctionTests: XCTestCase { 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 @@ -245,7 +247,7 @@ class PythonFunctionTests: XCTestCase { // test_method(): // - param1 // - param2 - + let BaseClass = PythonClass( "BaseClass", superclasses: [], @@ -257,7 +259,7 @@ class PythonFunctionTests: XCTestCase { `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) @@ -266,7 +268,7 @@ class PythonFunctionTests: XCTestCase { } ] ).pythonObject - + // Derived class accepts the following arguments: // __init__(): // - param2 @@ -276,7 +278,7 @@ class PythonFunctionTests: XCTestCase { // - param1 // - param2 // - param3 - + let DerivedClass = PythonClass( "DerivedClass", superclasses: [], @@ -284,7 +286,7 @@ class PythonFunctionTests: XCTestCase { "__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). @@ -294,44 +296,44 @@ class PythonFunctionTests: XCTestCase { ("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) diff --git a/Tests/PythonKitTests/PythonRuntimeTests.swift b/Tests/PythonKitTests/PythonRuntimeTests.swift index 9c059e6..90f1418 100644 --- a/Tests/PythonKitTests/PythonRuntimeTests.swift +++ b/Tests/PythonKitTests/PythonRuntimeTests.swift @@ -6,109 +6,136 @@ class PythonRuntimeTests: XCTestCase { XCTAssertGreaterThanOrEqual(Python.versionInfo.major, 2) XCTAssertGreaterThanOrEqual(Python.versionInfo.minor, 0) } - + func testPythonList() { let list: PythonObject = [0, 1, 2] XCTAssertEqual("[0, 1, 2]", list.description) XCTAssertEqual(3, Python.len(list)) XCTAssertEqual("[0, 1, 2]", Python.str(list)) - + let polymorphicList = PythonObject(["a", 2, true, 1.5]) XCTAssertEqual("a", polymorphicList[0]) XCTAssertEqual(2, polymorphicList[1]) XCTAssertEqual(true, polymorphicList[2]) XCTAssertEqual(1.5, polymorphicList[3]) XCTAssertEqual(1.5, polymorphicList[-1]) - + XCTAssertEqual(4, polymorphicList.count as Int) XCTAssertEqual(4, polymorphicList.checking.count!) XCTAssertEqual(4, polymorphicList.throwing.count!) - + polymorphicList[2] = 2 XCTAssertEqual(2, polymorphicList[2]) } - + #if !os(Windows) func testPythonDict() { let dict: PythonObject = ["a": 1, 1: 0.5] XCTAssertEqual(2, Python.len(dict)) XCTAssertEqual(1, dict["a"]) XCTAssertEqual(0.5, dict[1]) - + XCTAssertEqual(2, dict.count as Int) XCTAssertEqual(2, dict.checking.count!) XCTAssertEqual(2, dict.throwing.count!) - + dict["b"] = "c" XCTAssertEqual("c", dict["b"]) dict["b"] = "d" XCTAssertEqual("d", dict["b"]) + + // Dictionary initializer patch does not work on Python 2, but that + // version is no longer being actively supported. + guard Python.versionInfo.major >= 3 else { + return + } + + // Pandas DataFrame regression test spotted in Jupyter. This is + // non-deterministic, so repeat it several times to ensure the bug does + // not appear. + for _ in 0..<15 { + let records: [PythonObject] = [ + ["col 1": 3, "col 2": 5], + ["col 1": 8, "col 2": 2] + ] + let records_description = + "[{'col 1': 3, 'col 2': 5}, {'col 1': 8, 'col 2': 2}]" + XCTAssertEqual(String(describing: records), records_description) + + let records_alt: [PythonObject] = [ + ["col 1": 3, "col 2": 5, "col 3": 4], + ["col 1": 8, "col 2": 2, "col 3": 4] + ] + let records_alt_description = + "[{'col 1': 3, 'col 2': 5, 'col 3': 4}, {'col 1': 8, 'col 2': 2, 'col 3': 4}]" + XCTAssertEqual(String(describing: records_alt), records_alt_description) + } } - + func testRange() { let slice = PythonObject(5..<10) XCTAssertEqual(Python.slice(5, 10), slice) XCTAssertEqual(5, slice.start) XCTAssertEqual(10, slice.stop) - + let range = Range(slice) XCTAssertNotNil(range) XCTAssertEqual(5, range?.lowerBound) XCTAssertEqual(10, range?.upperBound) - + XCTAssertNil(Range(PythonObject(5...))) } - + func testPartialRangeFrom() { let slice = PythonObject(5...) XCTAssertEqual(Python.slice(5, Python.None), slice) XCTAssertEqual(5, slice.start) - + let range = PartialRangeFrom(slice) XCTAssertNotNil(range) XCTAssertEqual(5, range?.lowerBound) - + XCTAssertNil(PartialRangeFrom(PythonObject(..<5))) } - + func testPartialRangeUpTo() { let slice = PythonObject(..<5) XCTAssertEqual(Python.slice(5), slice) XCTAssertEqual(5, slice.stop) - + let range = PartialRangeUpTo(slice) XCTAssertNotNil(range) XCTAssertEqual(5, range?.upperBound) - + XCTAssertNil(PartialRangeUpTo(PythonObject(5...))) } #endif - + func testStrideable() { let strideTo = stride(from: PythonObject(0), to: 100, by: 2) XCTAssertEqual(0, strideTo.min()!) XCTAssertEqual(98, strideTo.max()!) XCTAssertEqual([0, 2, 4, 6, 8], Array(strideTo.prefix(5))) XCTAssertEqual([90, 92, 94, 96, 98], Array(strideTo.suffix(5))) - + let strideThrough = stride(from: PythonObject(0), through: 100, by: 2) XCTAssertEqual(0, strideThrough.min()!) XCTAssertEqual(100, strideThrough.max()!) XCTAssertEqual([0, 2, 4, 6, 8], Array(strideThrough.prefix(5))) XCTAssertEqual([92, 94, 96, 98, 100], Array(strideThrough.suffix(5))) } - + func testBinaryOps() { XCTAssertEqual(42, PythonObject(42)) XCTAssertEqual(42, PythonObject(2) + PythonObject(40)) XCTAssertEqual(2, PythonObject(2) * PythonObject(3) + PythonObject(-4)) - + XCTAssertEqual("abcdef", PythonObject("ab") + PythonObject("cde") + PythonObject("") + PythonObject("f")) XCTAssertEqual("ababab", PythonObject("ab") * 3) - + var x = PythonObject(2) x += 3 XCTAssertEqual(5, x) @@ -123,12 +150,12 @@ class PythonRuntimeTests: XCTestCase { } func testUnaryOps() { - var x = PythonObject(5) - x = -x - XCTAssertEqual(-5, x) - x = PythonObject(-5) - x = -x - XCTAssertEqual(5, x) + var x = PythonObject(5) + x = -x + XCTAssertEqual(-5, x) + x = PythonObject(-5) + x = -x + XCTAssertEqual(5, x) } func testComparable() { @@ -137,7 +164,7 @@ class PythonRuntimeTests: XCTestCase { let list: PythonObject = [-1, 10, 1, 0, 0] XCTAssertEqual([-1, 0, 0, 1, 10], list.sorted()) } - + #if !os(Windows) func testHashable() { func compareHashValues(_ x: PythonConvertible) { @@ -145,32 +172,32 @@ class PythonRuntimeTests: XCTestCase { let b = x.pythonObject XCTAssertEqual(a.hashValue, b.hashValue) } - + compareHashValues(1) compareHashValues(3.14) compareHashValues("asdf") compareHashValues(PythonObject(tupleOf: 1, 2, 3)) } #endif - + func testRangeIteration() { for (index, val) in Python.range(5).enumerated() { XCTAssertEqual(PythonObject(index), val) } } - + func testErrors() { XCTAssertThrowsError( try PythonObject(1).__truediv__.throwing.dynamicallyCall(withArguments: 0) ) { - guard let pythonError = $0 as? PythonError else { + guard case let PythonError.exception(exception, _) = $0 else { XCTFail("non-Python error: \($0)") return } - XCTAssertEqual(pythonError, PythonError.exception("division by zero", traceback: nil)) + XCTAssertEqual(exception.__class__.__name__, "ZeroDivisionError") } } - + #if !os(Windows) func testTuple() { let element1: PythonObject = 0 @@ -181,34 +208,34 @@ class PythonRuntimeTests: XCTestCase { let (pair1, pair2) = pair.tuple2 XCTAssertEqual(element1, pair1) XCTAssertEqual(element2, pair2) - + let triple = PythonObject(tupleOf: element1, element2, element3) let (triple1, triple2, triple3) = triple.tuple3 XCTAssertEqual(element1, triple1) XCTAssertEqual(element2, triple2) XCTAssertEqual(element3, triple3) - + let quadruple = PythonObject(tupleOf: element1, element2, element3, element4) let (quadruple1, quadruple2, quadruple3, quadruple4) = quadruple.tuple4 XCTAssertEqual(element1, quadruple1) XCTAssertEqual(element2, quadruple2) XCTAssertEqual(element3, quadruple3) XCTAssertEqual(element4, quadruple4) - + XCTAssertEqual(element2, quadruple[1]) } #endif - + func testMethodCalling() { let list: PythonObject = [1, 2] list.append(3) XCTAssertEqual([1, 2, 3], list) - + // Check method binding. let append = list.append append(4) XCTAssertEqual([1, 2, 3, 4], list) - + // Check *args/**kwargs behavior: `str.format(*args, **kwargs)`. let greeting: PythonObject = "{0} {first} {last}!" XCTAssertEqual("Hi John Smith!", @@ -216,7 +243,7 @@ class PythonRuntimeTests: XCTestCase { XCTAssertEqual("Hey Jane Doe!", greeting.format("Hey", first: "Jane", last: "Doe")) } - + func testConvertibleFromPython() { // Ensure that we cover the -1 case as this is used by Python // to signal conversion errors. @@ -225,7 +252,7 @@ class PythonRuntimeTests: XCTestCase { let five: PythonObject = 5 let half: PythonObject = 0.5 let string: PythonObject = "abc" - + #if !os(Windows) XCTAssertEqual(-1, Int(minusOne)) XCTAssertEqual(-1, Int8(minusOne)) @@ -235,10 +262,10 @@ class PythonRuntimeTests: XCTestCase { #endif XCTAssertEqual(-1.0, Float(minusOne)) XCTAssertEqual(-1.0, Double(minusOne)) - + XCTAssertEqual(0, Int(zero)) XCTAssertEqual(0.0, Double(zero)) - + XCTAssertEqual(5, UInt(five)) XCTAssertEqual(5, UInt8(five)) XCTAssertEqual(5, UInt16(five)) @@ -246,25 +273,25 @@ class PythonRuntimeTests: XCTestCase { XCTAssertEqual(5, UInt64(five)) XCTAssertEqual(5.0, Float(five)) XCTAssertEqual(5.0, Double(five)) - + XCTAssertEqual(0.5, Float(half)) XCTAssertEqual(0.5, Double(half)) // Python rounds down in this case. // XCTAssertEqual(0, Int(half)) - + XCTAssertEqual("abc", String(string)) - + XCTAssertNil(String(zero)) #if !os(Windows) XCTAssertNil(Int(string)) #endif XCTAssertNil(Double(string)) } - + func testPythonConvertible() { let minusOne: PythonObject = -1 let five: PythonObject = 5 - + XCTAssertEqual(minusOne, Int(-1).pythonObject) XCTAssertEqual(minusOne, Int8(-1).pythonObject) XCTAssertEqual(minusOne, Int16(-1).pythonObject) @@ -272,7 +299,7 @@ class PythonRuntimeTests: XCTestCase { XCTAssertEqual(minusOne, Int64(-1).pythonObject) XCTAssertEqual(minusOne, Float(-1).pythonObject) XCTAssertEqual(minusOne, Double(-1).pythonObject) - + XCTAssertEqual(five, UInt(5).pythonObject) XCTAssertEqual(five, UInt8(5).pythonObject) XCTAssertEqual(five, UInt16(5).pythonObject) @@ -281,13 +308,13 @@ class PythonRuntimeTests: XCTestCase { XCTAssertEqual(five, Float(5).pythonObject) XCTAssertEqual(five, Double(5).pythonObject) } - + // SR-9230: https://bugs.swift.org/browse/SR-9230 func testSR9230() { let pythonDict = Python.dict(a: "a", b: "b") XCTAssertEqual(Python.len(pythonDict), 2) } - + // TF-78: isType() consumed refcount for type objects like `PyBool_Type`. func testPythonRefCount() { let b: PythonObject = true @@ -334,4 +361,23 @@ class PythonRuntimeTests: XCTestCase { } XCTAssertEqual(bytes, otherBytes) } + + /// Tests an emulation of the Python `with` statement. + /// + /// Mirrors: + /// ```python + /// with open('temp', 'w') as opened_file: + /// opened_file.write('Contents') + /// with open('temp', 'r') as opened_file: + /// contents = opened_file.read() + /// ``` + func testWith() { + Python.with(Python.open("temp", "w")) { opened_file in + opened_file.write("Contents") + } + Python.with(Python.open("temp", "r")) { opened_file in + let contents = opened_file.read() + XCTAssertEqual("Contents", String(contents)!) + } + } }