diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 0000000..f0c855e --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +../Agents/Skills \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..f0c855e --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../Agents/Skills \ No newline at end of file diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1b57d86..a05389d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,36 +1,39 @@ name: Continuous Integration on: - - push - - pull_request + push: + branches: + - main + pull_request: + branches: + - main jobs: continuous-integration: + if: > + (github.event_name != 'pull_request' || github.event.pull_request.draft == false) && + !contains(github.event.head_commit.message || '', '[SKIP-CI]') && + !contains(github.event.pull_request.title || '', '[SKIP-CI]') strategy: matrix: os: - ubuntu-latest - - macOS-latest + - macos-latest runs-on: ${{ matrix.os }} steps: + - name: Show Swift Version + run: swift --version - uses: actions/checkout@v1 - - name: Installing Swift (Linux) - if: matrix.os == 'ubuntu-latest' - env: - SWIFT_VERSION: 5.1 - run: | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" - ~/.swiftenv/bin/swiftenv global $SWIFT_VERSION - sudo ln -s ~/.swiftenv/shims/swift /usr/bin/swift - - name: Installing Swift (macOS) - if: matrix.os == 'macOS-latest' + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Configure PythonKit + shell: bash run: | - sudo xcode-select -s /Applications/Xcode_11.app/Contents/Developer - - name: Test (Python 2) - env: - PYTHON_VERSION: 2 - run: swift test --enable-test-discovery - - name: Test (Python 3) + python_library="$(find -L "$pythonLocation" \( -name 'libpython3.13*.dylib' -o -name 'libpython3.13*.so' \) -print -quit)" + test -n "$python_library" + echo "PYTHON_LIBRARY=$python_library" >> "$GITHUB_ENV" + - name: Test + run: swift test env: - PYTHON_VERSION: 3 - run: swift test --enable-test-discovery + PYTHON_LOADER_LOGGING: TRUE diff --git a/.gitignore b/.gitignore index 9e8d8ee..3d50865 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ !.travis.yml # Swift -build/ -Package.resolved +/build/ +/Package.resolved # Python *.pyc @@ -15,6 +15,7 @@ Package.resolved # Temporary Items *.tmp *.tmp.* +temp # Virtual Environments /venv*/ @@ -23,5 +24,10 @@ Package.resolved *.override.* # Extra Directories -/Assets -/Extra +/Assets/ +/Extra/ + +# Xcode +xcuserdata/ +*.xcscmblueprint +*.xccheckout diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..5114dca --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +55307D11-9B02-4882-B275-49222602DC5E diff --git a/.open_source b/.open_source new file mode 100644 index 0000000..e69de29 diff --git a/.public_project b/.public_project new file mode 100644 index 0000000..e69de29 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2538ff9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Agents + +This is an open-source Swift package with a public API. + +## Project Notes + +- Read `README.md` for project-specific architecture, targets, and conventions. +- Keep project-specific guidance in `README.md` or source-local documentation, not in `AGENTS.md`. + +## Public Repository + +**IMPORTANT**: This is a public repository. Do not add internal or confidential information, including private project names, internal consumers, local paths, credentials, or content from private repositories or conversations. Use public, self-contained examples and dependencies. + +## Public API + +Modify this repository with extra care. Preserve its public APIs, behavior, supported platforms, toolchain requirements, and stable URLs. Do not introduce breaking changes without a detailed compatibility review and explicit authorization. Review source, tests, documentation, generated files, agent instructions, and commit or pull-request text for information disclosure before publishing. diff --git a/Agents/Instructions/.gitkeep b/Agents/Instructions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Agents/Skills/.gitkeep b/Agents/Skills/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Package.swift b/Package.swift index 064c879..2f5b7f0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,36 +1,26 @@ -// swift-tools-version:5.0 +// swift-tools-version:6.0 import PackageDescription let package = Package( - name: "PythonTool", + name: "PythonKit", products: [ - .executable( - name: "PythonTool", - targets: ["PythonTool"] - ), .library( name: "PythonKit", targets: ["PythonKit"] ) ], - dependencies: [ - .package(url: "https://github.com/pvieito/CommandLineKit.git", .branch("master")), - .package(url: "https://github.com/pvieito/LoggerKit.git", .branch("master")) - ], targets: [ - .target( - name: "PythonTool", - dependencies: ["LoggerKit", "CommandLineKit", "PythonKit"], - path: "PythonTool" - ), .target( name: "PythonKit", path: "PythonKit" ), .testTarget( name: "PythonKitTests", - dependencies: ["PythonKit"] + dependencies: [ + "PythonKit", + ] ), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/PythonKit/Info.plist b/PythonKit/Info.plist deleted file mode 100644 index 1dcdd98..0000000 --- a/PythonKit/Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSHumanReadableCopyright - Copyright © 2018 Pedro José Pereira Vieito. All rights reserved. - NSPrincipalClass - - - diff --git a/PythonKit/NumpyConversion.swift b/PythonKit/NumpyConversion.swift index 37b8588..e0c81a2 100644 --- a/PythonKit/NumpyConversion.swift +++ b/PythonKit/NumpyConversion.swift @@ -17,142 +17,142 @@ /// The `numpy` Python module. /// Note: Global variables are lazy, so the following declaration won't produce -// a Python import error until it is first used. +/// a Python import error until it is first used. private let np = Python.import("numpy") private let ctypes = Python.import("ctypes") /// A type that can be initialized from a `numpy.ndarray` instance represented /// as a `PythonObject`. public protocol ConvertibleFromNumpyArray { - init?(numpy: PythonObject) + init?(numpy: PythonObject) } /// A type that is bitwise compatible with one or more NumPy scalar types. public protocol NumpyScalarCompatible { - /// The NumPy scalar types that this type is bitwise compatible with. Must - /// be nonempty. - static var numpyScalarTypes: [PythonObject] { get } - /// The Python `ctypes` scalar type corresponding to this type. - static var ctype: PythonObject { get } + /// The NumPy scalar types that this type is bitwise compatible with. Must + /// be nonempty. + static var numpyScalarTypes: [PythonObject] { get } + /// The Python `ctypes` scalar type corresponding to this type. + static var ctype: PythonObject { get } } extension Bool : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.bool_, Python.bool] - public static var ctype: PythonObject { return ctypes.c_bool } + public static let numpyScalarTypes = [np.bool_, Python.bool] + public static var ctype: PythonObject { return ctypes.c_bool } } extension UInt8 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.uint8] - public static var ctype: PythonObject { return ctypes.c_uint8 } + public static let numpyScalarTypes = [np.uint8] + public static var ctype: PythonObject { return ctypes.c_uint8 } } extension Int8 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.int8] - public static var ctype: PythonObject { return ctypes.c_int8 } + public static let numpyScalarTypes = [np.int8] + public static var ctype: PythonObject { return ctypes.c_int8 } } extension UInt16 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.uint16] - public static var ctype: PythonObject { return ctypes.c_uint16 } + public static let numpyScalarTypes = [np.uint16] + public static var ctype: PythonObject { return ctypes.c_uint16 } } extension Int16 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.int16] - public static var ctype: PythonObject { return ctypes.c_int16 } + public static let numpyScalarTypes = [np.int16] + public static var ctype: PythonObject { return ctypes.c_int16 } } extension UInt32 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.uint32] - public static var ctype: PythonObject { return ctypes.c_uint32 } + public static let numpyScalarTypes = [np.uint32] + public static var ctype: PythonObject { return ctypes.c_uint32 } } extension Int32 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.int32] - public static var ctype: PythonObject { return ctypes.c_int32 } + public static let numpyScalarTypes = [np.int32] + public static var ctype: PythonObject { return ctypes.c_int32 } } extension UInt64 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.uint64] - public static var ctype: PythonObject { return ctypes.c_uint64 } + public static let numpyScalarTypes = [np.uint64] + public static var ctype: PythonObject { return ctypes.c_uint64 } } extension Int64 : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.int64] - public static var ctype: PythonObject { return ctypes.c_int64 } + public static let numpyScalarTypes = [np.int64] + public static var ctype: PythonObject { return ctypes.c_int64 } } extension Float : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.float32] - public static var ctype: PythonObject { return ctypes.c_float } + public static let numpyScalarTypes = [np.float32] + public static var ctype: PythonObject { return ctypes.c_float } } extension Double : NumpyScalarCompatible { - public static let numpyScalarTypes = [np.float64] - public static var ctype: PythonObject { return ctypes.c_double } + public static let numpyScalarTypes = [np.float64] + public static var ctype: PythonObject { return ctypes.c_double } } extension Array : ConvertibleFromNumpyArray - where Element : NumpyScalarCompatible { - /// Creates an `Array` with the same shape and scalars as the specified - /// `numpy.ndarray` instance. - /// - /// - Parameter numpyArray: The `numpy.ndarray` instance to convert. - /// - Precondition: The `numpy` Python package must be installed. - /// - Returns: `numpyArray` converted to an `Array`. Returns `nil` if - /// `numpyArray` is not 1-D or does not have a compatible scalar `dtype`. - public init?(numpy numpyArray: PythonObject) { - // Check if input is a `numpy.ndarray` instance. - guard Python.isinstance(numpyArray, np.ndarray) == true else { - return nil +where Element : NumpyScalarCompatible { + /// Creates an `Array` with the same shape and scalars as the specified + /// `numpy.ndarray` instance. + /// + /// - Parameter numpyArray: The `numpy.ndarray` instance to convert. + /// - Precondition: The `numpy` Python package must be installed. + /// - Returns: `numpyArray` converted to an `Array`. Returns `nil` if + /// `numpyArray` is not 1-D or does not have a compatible scalar `dtype`. + public init?(numpy numpyArray: PythonObject) { + // Check if input is a `numpy.ndarray` instance. + guard Python.isinstance(numpyArray, np.ndarray) == true else { + return nil + } + // Check if the dtype of the `ndarray` is compatible with the `Element` + // type. + 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 + } + guard let ptr = UnsafePointer(bitPattern: ptrVal) else { + fatalError("numpy.ndarray data pointer was nil") + } + // This code avoids constructing and initialize from `UnsafeBufferPointer` + // because that uses the `init(_ elements: S)` initializer, + // which performs unnecessary copying. + let dummyPointer = UnsafeMutablePointer.allocate(capacity: 1) + let scalarCount = shape.reduce(1, *) + self.init(repeating: dummyPointer.move(), count: scalarCount) + dummyPointer.deallocate() + withUnsafeMutableBufferPointer { buffPtr in + buffPtr.baseAddress!.update(from: ptr, count: scalarCount) + } } - // Check if the dtype of the `ndarray` is compatible with the `Element` - // type. - 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 - } - guard let ptr = UnsafePointer(bitPattern: ptrVal) else { - fatalError("numpy.ndarray data pointer was nil") - } - // This code avoids constructing and initialize from `UnsafeBufferPointer` - // because that uses the `init(_ elements: S)` initializer, - // which performs unnecessary copying. - let dummyPointer = UnsafeMutablePointer.allocate(capacity: 1) - let scalarCount = shape.reduce(1, *) - self.init(repeating: dummyPointer.move(), count: scalarCount) - dummyPointer.deallocate() - withUnsafeMutableBufferPointer { buffPtr in - buffPtr.baseAddress!.assign(from: ptr, count: scalarCount) - } - } } public extension Array where Element : NumpyScalarCompatible { - /// Creates a 1-D `numpy.ndarray` instance with the same scalars as this - /// `Array`. - /// - /// - Precondition: The `numpy` Python package must be installed. - func makeNumpyArray() -> PythonObject { - return withUnsafeBytes { bytes in - let data = ctypes.cast(Int(bitPattern: bytes.baseAddress), - ctypes.POINTER(Element.ctype)) - let ndarray = np.ctypeslib.as_array(data, shape: PythonObject(tupleOf: count)) - return np.copy(ndarray) + /// Creates a 1-D `numpy.ndarray` instance with the same scalars as this + /// `Array`. + /// + /// - Precondition: The `numpy` Python package must be installed. + func makeNumpyArray() -> PythonObject { + return withUnsafeBytes { bytes in + let data = ctypes.cast(Int(bitPattern: bytes.baseAddress), + ctypes.POINTER(Element.ctype)) + let ndarray = np.ctypeslib.as_array(data, shape: PythonObject(tupleOf: count)) + return np.copy(ndarray) + } } - } } diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 9b96916..35d2fcb 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -25,6 +25,7 @@ /// Typealias used when passing or returning a `PyObject` pointer with /// implied ownership. +@usableFromInline typealias OwnedPyObjectPointer = PyObjectPointer /// A primitive reference to a Python C API `PyObject`. @@ -37,34 +38,34 @@ typealias OwnedPyObjectPointer = PyObjectPointer // implement move semantics. @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. - init(_ pointer: OwnedPyObjectPointer) { - self.pointer = pointer - Py_IncRef(pointer) - } + 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. + init(_ pointer: OwnedPyObjectPointer) { + 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 - } + // This `PyReference` adopts the +1 reference and will decrement it in the + // future. + init(consuming pointer: PyObjectPointer) { + self.pointer = pointer + } - deinit { - Py_DecRef(pointer) - } + deinit { + Py_DecRef(pointer) + } - var borrowedPyObject: PyObjectPointer { - return pointer - } + var borrowedPyObject: PyObjectPointer { + return pointer + } - var ownedPyObject: OwnedPyObjectPointer { - Py_IncRef(pointer) - return pointer - } + var ownedPyObject: OwnedPyObjectPointer { + Py_IncRef(pointer) + return pointer + } } //===----------------------------------------------------------------------===// @@ -87,58 +88,58 @@ final class PyReference { @dynamicCallable @dynamicMemberLookup public struct PythonObject { - /// The underlying `PyReference`. - fileprivate var reference: PyReference + /// The underlying `PyReference`. + fileprivate var reference: PyReference - @usableFromInline - init(_ pointer: PyReference) { - reference = pointer - } + @usableFromInline + init(_ pointer: PyReference) { + reference = pointer + } - /// Creates a new instance and a new reference. - init(_ pointer: OwnedPyObjectPointer) { - reference = PyReference(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) - } + /// 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 borrowedPyObject: PyObjectPointer { + return reference.borrowedPyObject + } - fileprivate var ownedPyObject: OwnedPyObjectPointer { - return reference.ownedPyObject - } + fileprivate var ownedPyObject: OwnedPyObjectPointer { + return reference.ownedPyObject + } } // Make `print(python)` print a pretty form of the `PythonObject`. extension PythonObject : CustomStringConvertible { - /// A textual description of this `PythonObject`, produced by `Python.str`. - public var description: String { - // The `str` function is used here because it is designed to return - // human-readable descriptions of Python objects. The Python REPL also uses - // it for printing descriptions. - // `repr` is not used because it is not designed to be readable and takes - // too long for large objects. - return String(Python.str(self))! - } + /// A textual description of this `PythonObject`, produced by `Python.str`. + public var description: String { + // The `str` function is used here because it is designed to return + // human-readable descriptions of Python objects. The Python REPL also uses + // it for printing descriptions. + // `repr` is not used because it is not designed to be readable and takes + // too long for large objects. + return String(Python.str(self))! + } } // Make `PythonObject` show up nicely in the Xcode Playground results sidebar. extension PythonObject : CustomPlaygroundDisplayConvertible { - public var playgroundDescription: Any { - return description - } + public var playgroundDescription: Any { + return description + } } // Mirror representation, used by debugger/REPL. extension PythonObject : CustomReflectable { - public var customMirror: Mirror { - return Mirror(self, children: [], displayStyle: .struct) - } + public var customMirror: Mirror { + return Mirror(self, children: [], displayStyle: .struct) + } } //===----------------------------------------------------------------------===// @@ -147,27 +148,27 @@ extension PythonObject : CustomReflectable { /// A type whose values can be converted to a `PythonObject`. public protocol PythonConvertible { - /// A `PythonObject` instance representing this value. - var pythonObject: PythonObject { get } + /// A `PythonObject` instance representing this value. + var pythonObject: PythonObject { get } } public extension PythonObject { - /// Creates a new instance from a `PythonConvertible` value. - init(_ object: T) { - self.init(object.pythonObject) - } + /// Creates a new instance from a `PythonConvertible` value. + init(_ object: T) { + self.init(object.pythonObject) + } } /// Internal helpers to convert `PythonConvertible` values to owned and borrowed /// `PyObject` instances. These should not be made public. fileprivate extension PythonConvertible { - var borrowedPyObject: PyObjectPointer { - return pythonObject.borrowedPyObject - } + var borrowedPyObject: PyObjectPointer { + return pythonObject.borrowedPyObject + } - var ownedPyObject: OwnedPyObjectPointer { - return pythonObject.ownedPyObject - } + var ownedPyObject: OwnedPyObjectPointer { + return pythonObject.ownedPyObject + } } //===----------------------------------------------------------------------===// @@ -176,20 +177,20 @@ fileprivate extension PythonConvertible { /// A type that can be initialized from a `PythonObject`. public protocol ConvertibleFromPython { - /// Creates a new instance from the given `PythonObject`, if possible. - /// - Note: Conversion may fail if the given `PythonObject` instance is - /// incompatible (e.g. a Python `string` object cannot be converted into an - /// `Int`). - init?(_ object: PythonObject) + /// Creates a new instance from the given `PythonObject`, if possible. + /// - Note: Conversion may fail if the given `PythonObject` instance is + /// incompatible (e.g. a Python `string` object cannot be converted into an + /// `Int`). + init?(_ object: PythonObject) } // `PythonObject` is trivially `PythonConvertible`. extension PythonObject : PythonConvertible, ConvertibleFromPython { - public init(_ object: PythonObject) { - self.init(consuming: object.ownedPyObject) - } + public init(_ object: PythonObject) { + self.init(consuming: object.ownedPyObject) + } - public var pythonObject: PythonObject { return self } + public var pythonObject: PythonObject { return self } } //===----------------------------------------------------------------------===// @@ -197,68 +198,68 @@ extension PythonObject : PythonConvertible, ConvertibleFromPython { //===----------------------------------------------------------------------===// public extension PythonObject { - /// Returns a callable version of this `PythonObject`. When called, the result - /// throws a Swift error if the underlying Python function throws a Python - /// exception. - var throwing: ThrowingPythonObject { - return ThrowingPythonObject(self) - } + /// Returns a callable version of this `PythonObject`. When called, the result + /// throws a Swift error if the underlying Python function throws a Python + /// exception. + var throwing: ThrowingPythonObject { + return ThrowingPythonObject(self) + } } /// An error produced by a failable Python operation. 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. - /// - An incorrect number of arguments were provided to the callable Python - /// object. - /// - An invalid keyword argument was specified. - case invalidCall(PythonObject) - - /// A module import error. - case invalidModule(String) + /// 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. + /// - An incorrect number of arguments were provided to the callable Python + /// object. + /// - An invalid keyword argument was specified. + case invalidCall(PythonObject) + + /// A module import error. + case invalidModule(String) } extension PythonError : CustomStringConvertible { - public var description: String { - switch self { - case .exception(let e, let t): - var exceptionDescription = "Python exception: \(e)" - if let t = t { - let traceback = Python.import("traceback") - exceptionDescription += """ - \nTraceback: - \(PythonObject("").join(traceback.format_tb(t))) - """ - } - return exceptionDescription - case .invalidCall(let e): - return "Invalid Python call: \(e)" - case .invalidModule(let m): - return "Invalid Python module: \(m)" - } - } + public var description: String { + switch self { + case .exception(let e, let t): + var exceptionDescription = "Python exception: \(e)" + if let t = t { + let traceback = Python.import("traceback") + exceptionDescription += """ + \nTraceback: + \(PythonObject("").join(traceback.format_tb(t))) + """ + } + return exceptionDescription + case .invalidCall(let e): + return "Invalid Python call: \(e)" + case .invalidModule(let m): + return "Invalid Python module: \(m)" + } + } } // Reflect a Python error (which must be active) into a Swift error if one is // active. private func throwPythonErrorIfPresent() throws { - if PyErr_Occurred() == nil { return } + if PyErr_Occurred() == nil { return } - var type: PyObjectPointer? - var value: PyObjectPointer? - var traceback: PyObjectPointer? + var type: PyObjectPointer? + var value: PyObjectPointer? + var traceback: PyObjectPointer? - // Fetch the exception and clear the exception state. - PyErr_Fetch(&type, &value, &traceback) + // 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) } - throw PythonError.exception(resultObject, traceback: tracebackObject) + // 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) } + throw PythonError.exception(resultObject, traceback: tracebackObject) } /// A `PythonObject` wrapper that enables throwing method calls. @@ -269,133 +270,149 @@ private func throwPythonErrorIfPresent() throws { /// `x.throwing(arg1, arg2, ...)`. The methods will still be named /// `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. - /// - Parameter args: Positional arguments for the Python callable. - @discardableResult - public func dynamicallyCall( - 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. - /// - Parameter args: Positional arguments for the Python callable. - @discardableResult - 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() - throw PythonError.invalidCall(base) - } - 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. - /// - Parameter args: Positional or keyword arguments for the Python callable. - @discardableResult - public func dynamicallyCall( - withKeywordArguments args: - KeyValuePairs = [:] - ) throws -> PythonObject { - 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) - continue - } - // Initialize dictionary object if necessary. - if kwdictObject == nil { kwdictObject = PyDict_New()! } - // Add key-value pair to the dictionary object. - // TODO: Handle duplicate keys. - // In Python, `SyntaxError: keyword argument repeated` is thrown. - let k = PythonObject(key).ownedPyObject - let v = value.ownedPyObject - PyDict_SetItem(kwdictObject, k, v) - 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() - throw PythonError.invalidCall(base) - } - return PythonObject(consuming: result) - } - - /// Converts to a 2-tuple, if possible. - public var tuple2: (PythonObject, PythonObject)? { - let ct = base.checking - guard let elt0 = ct[0], let elt1 = ct[1] else { - return nil - } - return (elt0, elt1) - } - - /// Converts to a 3-tuple, if possible. - public var tuple3: (PythonObject, PythonObject, PythonObject)? { - let ct = base.checking - guard let elt0 = ct[0], let elt1 = ct[1], let elt2 = ct[2] else { - return nil - } - return (elt0, elt1, elt2) - } - - /// Converts to a 4-tuple, if possible. - public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { - let ct = base.checking - guard let elt0 = ct[0], let elt1 = ct[1], - let elt2 = ct[2], let elt3 = ct[3] else { - return nil - } - return (elt0, elt1, elt2, elt3) - } + 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. + /// - Parameter args: Positional arguments for the Python callable. + @discardableResult + public func dynamicallyCall( + 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. + /// - Parameter args: Positional arguments for the Python callable. + @discardableResult + 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() + throw PythonError.invalidCall(base) + } + 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. + /// - Parameter args: Positional or keyword arguments for the Python callable. + @discardableResult + public func dynamicallyCall( + withKeywordArguments args: + 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 + public func dynamicallyCall( + withKeywordArguments args: + [(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) + continue + } + // Initialize dictionary object if necessary. + if kwdictObject == nil { kwdictObject = PyDict_New()! } + // Add key-value pair to the dictionary object. + // TODO: Handle duplicate keys. + // In Python, `SyntaxError: keyword argument repeated` is thrown. + let k = PythonObject(key).ownedPyObject + let v = value.ownedPyObject + PyDict_SetItem(kwdictObject, k, v) + 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() + throw PythonError.invalidCall(base) + } + return PythonObject(consuming: result) + } + + /// Converts to a 2-tuple, if possible. + public var tuple2: (PythonObject, PythonObject)? { + let ct = base.checking + guard let elt0 = ct[0], let elt1 = ct[1] else { + return nil + } + return (elt0, elt1) + } + + /// Converts to a 3-tuple, if possible. + public var tuple3: (PythonObject, PythonObject, PythonObject)? { + let ct = base.checking + guard let elt0 = ct[0], let elt1 = ct[1], let elt2 = ct[2] else { + return nil + } + return (elt0, elt1, elt2) + } + + /// Converts to a 4-tuple, if possible. + public var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject)? { + let ct = base.checking + guard let elt0 = ct[0], let elt1 = ct[1], + let elt2 = ct[2], let elt3 = ct[3] else { + return nil + } + return (elt0, elt1, elt2, elt3) + } + + public var count: Int? { + base.checking.count + } } @@ -404,10 +421,10 @@ public struct ThrowingPythonObject { //===----------------------------------------------------------------------===// public extension PythonObject { - /// Returns a `PythonObject` wrapper capable of member accesses. - var checking: CheckingPythonObject { - return CheckingPythonObject(self) - } + /// Returns a `PythonObject` wrapper capable of member accesses. + var checking: CheckingPythonObject { + return CheckingPythonObject(self) + } } /// A `PythonObject` wrapper that enables member accesses. @@ -415,100 +432,104 @@ public extension PythonObject { /// fails, `nil` is returned. @dynamicMemberLookup 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 - defer { Py_DecRef(selfObject) } - guard let result = PyObject_GetAttrString(selfObject, name) else { - PyErr_Clear() - return nil - } - // `PyObject_GetAttrString` returns +1 result. - 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. - public subscript(key: [PythonConvertible]) -> PythonObject? { - get { - let keyObject = flattenedSubscriptIndices(key) - let selfObject = base.ownedPyObject - defer { - Py_DecRef(keyObject) - Py_DecRef(selfObject) - } - - // `PyObject_GetItem` returns +1 reference. - if let result = PyObject_GetItem(selfObject, keyObject) { - return PythonObject(consuming: result) - } - PyErr_Clear() - return nil - } - nonmutating set { - let keyObject = flattenedSubscriptIndices(key) - let selfObject = base.ownedPyObject - defer { - Py_DecRef(keyObject) - Py_DecRef(selfObject) - } - - if let newValue = newValue { - let newValueObject = newValue.ownedPyObject - PyObject_SetItem(selfObject, keyObject, newValueObject) - Py_DecRef(newValueObject) - } else { - // Assigning `nil` deletes the key, just like Swift dictionaries. - PyObject_DelItem(selfObject, keyObject) - } - } - } - - /// Access the element corresponding to the specified `PythonConvertible` - /// values representing a key. - /// - Note: This is equivalent to `object[key]` in Python. - public subscript(key: PythonConvertible...) -> PythonObject? { - get { - return self[key] - } - nonmutating set { - 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 { - return nil - } - 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 { - return nil - } - 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], - let elt2 = self[2], let elt3 = self[3] else { - return nil - } - return (elt0, elt1, elt2, elt3) - } + /// 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 + defer { Py_DecRef(selfObject) } + guard let result = PyObject_GetAttrString(selfObject, name) else { + PyErr_Clear() + return nil + } + // `PyObject_GetAttrString` returns +1 result. + 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. + public subscript(key: [PythonConvertible]) -> PythonObject? { + get { + let keyObject = flattenedSubscriptIndices(key) + let selfObject = base.ownedPyObject + defer { + Py_DecRef(keyObject) + Py_DecRef(selfObject) + } + + // `PyObject_GetItem` returns +1 reference. + if let result = PyObject_GetItem(selfObject, keyObject) { + return PythonObject(consuming: result) + } + PyErr_Clear() + return nil + } + nonmutating set { + let keyObject = flattenedSubscriptIndices(key) + let selfObject = base.ownedPyObject + defer { + Py_DecRef(keyObject) + Py_DecRef(selfObject) + } + + if let newValue = newValue { + let newValueObject = newValue.ownedPyObject + PyObject_SetItem(selfObject, keyObject, newValueObject) + Py_DecRef(newValueObject) + } else { + // Assigning `nil` deletes the key, just like Swift dictionaries. + PyObject_DelItem(selfObject, keyObject) + } + } + } + + /// Access the element corresponding to the specified `PythonConvertible` + /// values representing a key. + /// - Note: This is equivalent to `object[key]` in Python. + public subscript(key: PythonConvertible...) -> PythonObject? { + get { + return self[key] + } + nonmutating set { + 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 { + return nil + } + 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 { + return nil + } + 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], + let elt2 = self[2], let elt3 = self[3] else { + return nil + } + return (elt0, elt1, elt2, elt3) + } + + public var count: Int? { + Int(Python.len(base)) + } } //===----------------------------------------------------------------------===// @@ -518,100 +539,106 @@ public struct CheckingPythonObject { /// Converts an array of indices into a `PythonObject` representing a flattened /// index. private func flattenedSubscriptIndices( - _ indices: [PythonConvertible] -) -> OwnedPyObjectPointer { - if indices.count == 1 { - return indices[0].ownedPyObject - } - return pyTuple(indices.map { $0.pythonObject }) + _ indices: [PythonConvertible]) -> OwnedPyObjectPointer { + if indices.count == 1 { + return indices[0].ownedPyObject + } + return pyTuple(indices.map { $0.pythonObject }) } public extension PythonObject { - subscript(dynamicMember memberName: String) -> PythonObject { - get { - guard let member = checking[dynamicMember: memberName] else { - fatalError("Could not access PythonObject member '\(memberName)'") - } - return member - } - nonmutating set { - let selfObject = ownedPyObject - defer { Py_DecRef(selfObject) } - let valueObject = newValue.ownedPyObject - defer { Py_DecRef(valueObject) } - - if PyObject_SetAttrString(selfObject, memberName, valueObject) == -1 { - try! throwPythonErrorIfPresent() - fatalError(""" - Could not set PythonObject member '\(memberName)' to the specified \ - value - """) - } - } - } - - /// Access the element corresponding to the specified `PythonConvertible` - /// values representing a key. - /// - Note: This is equivalent to `object[key]` in Python. - subscript(key: PythonConvertible...) -> PythonObject { - get { - guard let item = checking[key] else { - fatalError(""" - Could not access PythonObject element corresponding to the specified \ - key values: \(key) - """) - } - return item - } - nonmutating set { - checking[key] = newValue - } - } - - /// Converts to a 2-tuple. - var tuple2: (PythonObject, PythonObject) { - guard let result = checking.tuple2 else { - fatalError("Could not convert PythonObject to a 2-element tuple") - } - return result - } - - /// Converts to a 3-tuple. - var tuple3: (PythonObject, PythonObject, PythonObject) { - guard let result = checking.tuple3 else { - fatalError("Could not convert PythonObject to a 3-element tuple") - } - return result - } - - /// Converts to a 4-tuple. - var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject) { - guard let result = checking.tuple4 else { - fatalError("Could not convert PythonObject to a 4-element tuple") - } - return result - } - - /// Call `self` with the specified positional arguments. - /// - Precondition: `self` must be a Python callable. - /// - Parameter args: Positional arguments for the Python callable. - @discardableResult - func dynamicallyCall( - 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. - @discardableResult - func dynamicallyCall( - withKeywordArguments args: - KeyValuePairs = [:] - ) -> PythonObject { - return try! throwing.dynamicallyCall(withKeywordArguments: args) - } + subscript(dynamicMember memberName: String) -> PythonObject { + get { + guard let member = checking[dynamicMember: memberName] else { + fatalError("Could not access PythonObject member '\(memberName)'") + } + return member + } + nonmutating set { + let selfObject = ownedPyObject + defer { Py_DecRef(selfObject) } + let valueObject = newValue.ownedPyObject + defer { Py_DecRef(valueObject) } + + if PyObject_SetAttrString(selfObject, memberName, valueObject) == -1 { + try! throwPythonErrorIfPresent() + fatalError(""" + Could not set PythonObject member '\(memberName)' to the specified \ + value + """) + } + } + } + + /// Access the element corresponding to the specified `PythonConvertible` + /// values representing a key. + /// - Note: This is equivalent to `object[key]` in Python. + subscript(key: PythonConvertible...) -> PythonObject { + get { + guard let item = checking[key] else { + fatalError(""" + Could not access PythonObject element corresponding to the specified \ + key values: \(key) + """) + } + return item + } + nonmutating set { + checking[key] = newValue + } + } + + /// Converts to a 2-tuple. + var tuple2: (PythonObject, PythonObject) { + guard let result = checking.tuple2 else { + fatalError("Could not convert PythonObject to a 2-element tuple") + } + return result + } + + /// Converts to a 3-tuple. + var tuple3: (PythonObject, PythonObject, PythonObject) { + guard let result = checking.tuple3 else { + fatalError("Could not convert PythonObject to a 3-element tuple") + } + return result + } + + /// Converts to a 4-tuple. + var tuple4: (PythonObject, PythonObject, PythonObject, PythonObject) { + guard let result = checking.tuple4 else { + fatalError("Could not convert PythonObject to a 4-element tuple") + } + return result + } + + /// Call `self` with the specified positional arguments. + /// - Precondition: `self` must be a Python callable. + /// - Parameter args: Positional arguments for the Python callable. + @discardableResult + func dynamicallyCall( + 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. + @discardableResult + func dynamicallyCall( + withKeywordArguments args: + 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 + func dynamicallyCall( + withKeywordArguments args: + [(key: String, value: PythonConvertible)] = []) -> PythonObject { + return try! throwing.dynamicallyCall(withKeywordArguments: args) + } } //===----------------------------------------------------------------------===// @@ -644,55 +671,66 @@ public let Python = PythonInterface() /// called `Python`. @dynamicMemberLookup public struct PythonInterface { - /// A dictionary of the Python builtins. - public let builtins: PythonObject - - init() { - Py_Initialize() // Initialize Python - builtins = PythonObject(PyEval_GetBuiltins()) - - // Runtime Fixes: - // - // Some Python modules expect to have at least one argument in `sys.argv`. - PyRun_SimpleString("import sys; 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 (`CommandLine.arguments[0]`). - PyRun_SimpleString(""" - import sys - import os - if sys.version_info.major == 3 and sys.platform == 'darwin': - sys.executable = os.path.join(sys.exec_prefix, 'bin', 'python3') - """) - } - - public func attemptImport(_ name: String) throws -> PythonObject { - guard let module = PyImport_ImportModule(name) else { - try throwPythonErrorIfPresent() - throw PythonError.invalidModule(name) - } - 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 - } + /// 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`: + 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: + if sys.version_info.major == 3 and sys.platform == "darwin": + 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() + throw PythonError.invalidModule(name) + } + 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__() + } } //===----------------------------------------------------------------------===// @@ -701,32 +739,31 @@ public struct PythonInterface { // Create a Python tuple object with the specified elements. private func pyTuple(_ vals: T) -> OwnedPyObjectPointer - where T.Element : PythonConvertible { - - let tuple = PyTuple_New(vals.count)! - for (index, element) in vals.enumerated() { - // `PyTuple_SetItem` steals the reference of the object stored. - PyTuple_SetItem(tuple, index, element.ownedPyObject) - } - return tuple +where T.Element : PythonConvertible { + let tuple = PyTuple_New(vals.count)! + for (index, element) in vals.enumerated() { + // `PyTuple_SetItem` steals the reference of the object stored. + PyTuple_SetItem(tuple, index, element.ownedPyObject) + } + return tuple } public extension PythonObject { - // Tuples require explicit support because tuple types cannot conform to - // protocols. - init(tupleOf elements: PythonConvertible...) { - self.init(tupleContentsOf: elements) - } + // Tuples require explicit support because tuple types cannot conform to + // protocols. + init(tupleOf elements: PythonConvertible...) { + self.init(tupleContentsOf: elements) + } - init(tupleContentsOf elements: T) + init(tupleContentsOf elements: T) where T.Element == PythonConvertible { - self.init(consuming: pyTuple(elements.map { $0.pythonObject })) - } + self.init(consuming: pyTuple(elements.map { $0.pythonObject })) + } - init(tupleContentsOf elements: T) + init(tupleContentsOf elements: T) where T.Element : PythonConvertible { - self.init(consuming: pyTuple(elements)) - } + self.init(consuming: pyTuple(elements)) + } } //===----------------------------------------------------------------------===// @@ -737,130 +774,130 @@ public extension PythonObject { /// type descriptor passed in as 'type'. private func isType(_ object: PythonObject, type: PyObjectPointer) -> Bool { - let typePyRef = PythonObject(type) + let typePyRef = PythonObject(type) - let result = Python.isinstance(object, typePyRef) + 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) } + // 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 + // Anything not equal to `Py_ZeroStruct` is truthy. + return pyObject != _Py_ZeroStruct } extension Bool : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard isType(pythonObject, type: PyBool_Type) else { return nil } + public init?(_ pythonObject: PythonObject) { + guard isType(pythonObject, type: PyBool_Type) else { return nil } - let pyObject = pythonObject.ownedPyObject - defer { Py_DecRef(pyObject) } + let pyObject = pythonObject.ownedPyObject + defer { Py_DecRef(pyObject) } - self = pyObject == _Py_TrueStruct - } + self = pyObject == _Py_TrueStruct + } - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return PythonObject(consuming: PyBool_FromLong(self ? 1 : 0)) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return PythonObject(consuming: PyBool_FromLong(self ? 1 : 0)) + } } 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 + 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) } - self.init(cString: cString) - } - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - let v = utf8CString.withUnsafeBufferPointer { - // 1 is subtracted from the C string length to trim the trailing null - // character (`\0`). - PyString_FromStringAndSize($0.baseAddress, $0.count - 1)! + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + let v = utf8CString.withUnsafeBufferPointer { + // 1 is subtracted from the C string length to trim the trailing null + // character (`\0`). + PyString_FromStringAndSize($0.baseAddress, $0.count - 1)! + } + return PythonObject(consuming: v) } - return PythonObject(consuming: v) - } } fileprivate extension PythonObject { - // Converts a `PythonObject` to the given type by applying the appropriate - // converter function and checking the error value. - func converted( - withError errorValue: T, by converter: (OwnedPyObjectPointer) -> T - ) -> T? { - let pyObject = ownedPyObject - defer { Py_DecRef(pyObject) } - - assert(PyErr_Occurred() == nil, - "Python error occurred somewhere but wasn't handled") + // Converts a `PythonObject` to the given type by applying the appropriate + // converter function and checking the error value. + func converted( + withError errorValue: T, by converter: (OwnedPyObjectPointer) -> T + ) -> 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 - let value = converter(pyObject) - guard value != errorValue || PyErr_Occurred() == nil else { - PyErr_Clear() - return nil } - return value - - } } extension Int : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - // `PyInt_AsLong` return -1 and sets an error if the Python object is not - // integer compatible. - guard let value = pythonObject.converted(withError: -1, - by: PyInt_AsLong) else { - return nil + public init?(_ pythonObject: PythonObject) { + // `PyInt_AsLong` return -1 and sets an error if the Python object is not + // integer compatible. + guard let value = pythonObject.converted( + withError: -1, by: PyInt_AsLong) else { + return nil + } + self = value } - self = value - } - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return PythonObject(consuming: PyInt_FromLong(self)) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return PythonObject(consuming: PyInt_FromLong(self)) + } } extension UInt : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - // `PyInt_AsUnsignedLongMask` isn't documented as such, but in fact it does - // return -1 and set an error if the Python object is not integer - // compatible. - guard let value = pythonObject.converted( - withError: ~0, by: PyInt_AsUnsignedLongMask) else { - return nil + public init?(_ pythonObject: PythonObject) { + // `PyInt_AsUnsignedLongMask` isn't documented as such, but in fact it does + // return -1 and set an error if the Python object is not integer + // compatible. + guard let value = pythonObject.converted( + withError: ~0, by: PyInt_AsUnsignedLongMask) else { + return nil + } + self = value } - self = value - } - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return PythonObject(consuming: PyInt_FromSize_t(self)) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return PythonObject(consuming: PyInt_FromSize_t(self)) + } } extension Double : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - // `PyFloat_AsDouble` return -1 and sets an error if the Python object is - // not float compatible. - guard let value = pythonObject.converted(withError: -1, - by: PyFloat_AsDouble) else { - return nil + public init?(_ pythonObject: PythonObject) { + // `PyFloat_AsDouble` return -1 and sets an error if the Python object is + // not float compatible. + guard let value = pythonObject.converted( + withError: -1, by: PyFloat_AsDouble) else { + return nil + } + self = value } - self = value - } - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return PythonObject(consuming: PyFloat_FromDouble(self)) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return PythonObject(consuming: PyFloat_FromDouble(self)) + } } //===----------------------------------------------------------------------===// @@ -871,104 +908,104 @@ extension Double : PythonConvertible, ConvertibleFromPython { // implementation. extension Int8 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = Int(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = Int(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return Int(self).pythonObject - } + public var pythonObject: PythonObject { + return Int(self).pythonObject + } } extension Int16 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = Int(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = Int(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return Int(self).pythonObject - } + public var pythonObject: PythonObject { + return Int(self).pythonObject + } } extension Int32 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = Int(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = Int(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return Int(self).pythonObject - } + public var pythonObject: PythonObject { + return Int(self).pythonObject + } } extension Int64 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = Int(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = Int(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return Int(self).pythonObject - } + public var pythonObject: PythonObject { + return Int(self).pythonObject + } } extension UInt8 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = UInt(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = UInt(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return UInt(self).pythonObject - } + public var pythonObject: PythonObject { + return UInt(self).pythonObject + } } extension UInt16 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = UInt(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = UInt(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return UInt(self).pythonObject - } + public var pythonObject: PythonObject { + return UInt(self).pythonObject + } } extension UInt32 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = UInt(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = UInt(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return UInt(self).pythonObject - } + public var pythonObject: PythonObject { + return UInt(self).pythonObject + } } extension UInt64 : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let i = UInt(pythonObject) else { return nil } - self.init(i) - } + public init?(_ pythonObject: PythonObject) { + guard let i = UInt(pythonObject) else { return nil } + self.init(i) + } - public var pythonObject: PythonObject { - return UInt(self).pythonObject - } + public var pythonObject: PythonObject { + return UInt(self).pythonObject + } } // `Float` is `PythonConvertible` via the `Double` implementation. extension Float : PythonConvertible, ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard let v = Double(pythonObject) else { return nil } - self.init(v) - } + public init?(_ pythonObject: PythonObject) { + guard let v = Double(pythonObject) else { return nil } + self.init(v) + } - public var pythonObject: PythonObject { - return Double(self).pythonObject - } + public var pythonObject: PythonObject { + return Double(self).pythonObject + } } //===----------------------------------------------------------------------===// @@ -976,9 +1013,9 @@ extension Float : PythonConvertible, ConvertibleFromPython { //===----------------------------------------------------------------------===// extension Optional : PythonConvertible where Wrapped : PythonConvertible { - public var pythonObject: PythonObject { - return self?.pythonObject ?? Python.None - } + public var pythonObject: PythonObject { + return self?.pythonObject ?? Python.None + } } //===----------------------------------------------------------------------===// @@ -986,17 +1023,17 @@ extension Optional : PythonConvertible where Wrapped : PythonConvertible { //===----------------------------------------------------------------------===// extension Optional : ConvertibleFromPython - where Wrapped : ConvertibleFromPython { - public init?(_ object: PythonObject) { - if object == Python.None { - self = .none - } else { - guard let converted = Wrapped(object) else { - return nil - } - self = .some(converted) +where Wrapped : ConvertibleFromPython { + public init?(_ object: PythonObject) { + if object == Python.None { + self = .none + } else { + guard let converted = Wrapped(object) else { + return nil + } + self = .some(converted) + } } - } } //===----------------------------------------------------------------------===// @@ -1007,70 +1044,71 @@ extension Optional : ConvertibleFromPython // `Array` conditionally conforms to `PythonConvertible` if the `Element` // associated type does. extension Array : PythonConvertible where Element : PythonConvertible { - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - let list = PyList_New(count)! - for (index, element) in enumerated() { - // `PyList_SetItem` steals the reference of the object stored. - _ = PyList_SetItem(list, index, element.ownedPyObject) + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + let list = PyList_New(count)! + for (index, element) in enumerated() { + // `PyList_SetItem` steals the reference of the object stored. + _ = PyList_SetItem(list, index, element.ownedPyObject) + } + return PythonObject(consuming: list) } - return PythonObject(consuming: list) - } } extension Array : ConvertibleFromPython where Element : ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - self = [] - for elementObject in pythonObject { - guard let element = Element(elementObject) else { return nil } - append(element) + public init?(_ pythonObject: PythonObject) { + self = [] + for elementObject in pythonObject { + guard let element = Element(elementObject) else { return nil } + append(element) + } } - } } // `Dictionary` conditionally conforms to `PythonConvertible` if the `Key` and // `Value` associated types do. extension Dictionary : PythonConvertible - where Key : PythonConvertible, Value : PythonConvertible { - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - let dict = PyDict_New()! - for (key, value) in self { - let k = key.ownedPyObject - let v = value.ownedPyObject - PyDict_SetItem(dict, k, v) - Py_DecRef(k) - Py_DecRef(v) +where Key : PythonConvertible, Value : PythonConvertible { + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + let dict = PyDict_New()! + for (key, value) in self { + let k = key.ownedPyObject + let v = value.ownedPyObject + PyDict_SetItem(dict, k, v) + Py_DecRef(k) + Py_DecRef(v) + } + return PythonObject(consuming: dict) } - return PythonObject(consuming: dict) - } } 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 { - // If any key or value is not convertible to the corresponding Swift - // type, then the entire dictionary is not convertible. - if let swiftKey = Key(PythonObject(key!)), - let swiftValue = Value(PythonObject(value!)) { - // It is possible that there are duplicate keys after conversion. We - // silently allow duplicate keys and pick a nondeterministic result if - // there is a collision. - self[swiftKey] = swiftValue - } else { - return nil - } - } - } +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 { + // If any key or value is not convertible to the corresponding Swift + // type, then the entire dictionary is not convertible. + if let swiftKey = Key(PythonObject(key!)), + let swiftValue = Value(PythonObject(value!)) { + // It is possible that there are duplicate keys after conversion. We + // silently allow duplicate keys and pick a nondeterministic result if + // there is a collision. + self[swiftKey] = swiftValue + } else { + return nil + } + } + } } //===----------------------------------------------------------------------===// @@ -1079,62 +1117,62 @@ extension Dictionary : ConvertibleFromPython //===----------------------------------------------------------------------===// extension Range : PythonConvertible where Bound : PythonConvertible { - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return Python.slice(lowerBound, upperBound, Python.None) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return Python.slice(lowerBound, upperBound, Python.None) + } } extension Range : ConvertibleFromPython where Bound : ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard isType(pythonObject, type: PySlice_Type) else { return nil } - guard let lowerBound = Bound(pythonObject.start), - let upperBound = Bound(pythonObject.stop) else { - return nil + public init?(_ pythonObject: PythonObject) { + guard isType(pythonObject, type: PySlice_Type) else { return nil } + guard let lowerBound = Bound(pythonObject.start), + let upperBound = Bound(pythonObject.stop) else { + return nil + } + guard pythonObject.step == Python.None else { return nil } + self.init(uncheckedBounds: (lowerBound, upperBound)) } - guard pythonObject.step == Python.None else { return nil } - self.init(uncheckedBounds: (lowerBound, upperBound)) - } } extension PartialRangeFrom : PythonConvertible where Bound : PythonConvertible { - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return Python.slice(lowerBound, Python.None, Python.None) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return Python.slice(lowerBound, Python.None, Python.None) + } } extension PartialRangeFrom : ConvertibleFromPython - where Bound : ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard isType(pythonObject, type: PySlice_Type) else { return nil } - guard let lowerBound = Bound(pythonObject.start) else { return nil } - guard pythonObject.stop == Python.None, - pythonObject.step == Python.None else { - return nil +where Bound : ConvertibleFromPython { + public init?(_ pythonObject: PythonObject) { + guard isType(pythonObject, type: PySlice_Type) else { return nil } + guard let lowerBound = Bound(pythonObject.start) else { return nil } + guard pythonObject.stop == Python.None, + pythonObject.step == Python.None else { + return nil + } + self.init(lowerBound) } - self.init(lowerBound) - } } extension PartialRangeUpTo : PythonConvertible where Bound : PythonConvertible { - public var pythonObject: PythonObject { - _ = Python // Ensure Python is initialized. - return Python.slice(Python.None, upperBound, Python.None) - } + public var pythonObject: PythonObject { + _ = Python // Ensure Python is initialized. + return Python.slice(Python.None, upperBound, Python.None) + } } extension PartialRangeUpTo : ConvertibleFromPython - where Bound : ConvertibleFromPython { - public init?(_ pythonObject: PythonObject) { - guard isType(pythonObject, type: PySlice_Type) else { return nil } - guard let upperBound = Bound(pythonObject.stop) else { return nil } - guard pythonObject.start == Python.None, - pythonObject.step == Python.None else { - return nil +where Bound : ConvertibleFromPython { + public init?(_ pythonObject: PythonObject) { + guard isType(pythonObject, type: PySlice_Type) else { return nil } + guard let upperBound = Bound(pythonObject.stop) else { return nil } + guard pythonObject.start == Python.None, + pythonObject.step == Python.None else { + return nil + } + self.init(upperBound) } - self.init(upperBound) - } } //===----------------------------------------------------------------------===// @@ -1142,210 +1180,755 @@ extension PartialRangeUpTo : ConvertibleFromPython //===----------------------------------------------------------------------===// private typealias PythonBinaryOp = - (OwnedPyObjectPointer?, OwnedPyObjectPointer?) -> OwnedPyObjectPointer? + (OwnedPyObjectPointer?, OwnedPyObjectPointer?) -> OwnedPyObjectPointer? +private typealias PythonUnaryOp = + (OwnedPyObjectPointer?) -> OwnedPyObjectPointer? private func performBinaryOp( - _ op: PythonBinaryOp, lhs: PythonObject, rhs: PythonObject -) -> PythonObject { - let result = op(lhs.borrowedPyObject, rhs.borrowedPyObject) - // If binary operation fails (e.g. due to `TypeError`), throw an exception. - try! throwPythonErrorIfPresent() - return PythonObject(consuming: result!) + _ op: PythonBinaryOp, lhs: PythonObject, rhs: PythonObject) -> PythonObject { + let result = op(lhs.borrowedPyObject, rhs.borrowedPyObject) + // If binary operation fails (e.g. due to `TypeError`), throw an exception. + try! throwPythonErrorIfPresent() + return PythonObject(consuming: result!) +} + +private func performUnaryOp( + _ op: PythonUnaryOp, operand: PythonObject) -> PythonObject { + let result = op(operand.borrowedPyObject) + // If unary operation fails (e.g. due to `TypeError`), throw an exception. + try! throwPythonErrorIfPresent() + return PythonObject(consuming: result!) } 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_Add, lhs: lhs, rhs: rhs) + } - static func - (lhs: PythonObject, rhs: PythonObject) -> PythonObject { - return performBinaryOp(PyNumber_Subtract, lhs: lhs, rhs: rhs) - } + static func - (lhs: PythonObject, rhs: PythonObject) -> PythonObject { + return performBinaryOp(PyNumber_Subtract, lhs: lhs, rhs: rhs) + } + + 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: PythonObject, rhs: PythonObject) -> PythonObject { - return performBinaryOp(PyNumber_Multiply, 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: PythonObject, rhs: PythonObject) -> PythonObject { - return performBinaryOp(PyNumber_TrueDivide, lhs: lhs, rhs: rhs) - } + static func /= (lhs: inout PythonObject, rhs: PythonObject) { + lhs = performBinaryOp(PyNumber_InPlaceTrueDivide, lhs: lhs, rhs: rhs) + } +} + +public extension PythonObject { + static func & (lhs: PythonObject, rhs: PythonObject) -> PythonObject { + return performBinaryOp(PyNumber_And, lhs: lhs, rhs: rhs) + } + + static func | (lhs: PythonObject, rhs: PythonObject) -> PythonObject { + return performBinaryOp(PyNumber_Or, lhs: lhs, rhs: rhs) + } + + static func ^ (lhs: PythonObject, rhs: PythonObject) -> PythonObject { + return performBinaryOp(PyNumber_Xor, 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_InPlaceAnd, 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_InPlaceOr, 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_InPlaceXor, lhs: lhs, rhs: rhs) + } - static func /= (lhs: inout PythonObject, rhs: PythonObject) { - lhs = performBinaryOp(PyNumber_InPlaceTrueDivide, lhs: lhs, rhs: rhs) - } + static prefix func ~ (_ operand: Self) -> Self { + return performUnaryOp(PyNumber_Invert, operand: operand) + } } extension PythonObject : SignedNumeric { - public init(exactly value: T) { - self.init(Int(value)) - } + public init(exactly value: T) { + self.init(Int(value)) + } - public typealias Magnitude = PythonObject + public typealias Magnitude = PythonObject - public var magnitude: PythonObject { - return self < 0 ? -self : self - } + 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). + public static prefix func - (_ operand: Self) -> Self { + return performUnaryOp(PyNumber_Negative, operand: operand) + } } extension PythonObject : Strideable { - public typealias Stride = PythonObject + public typealias Stride = PythonObject - public func distance(to other: PythonObject) -> Stride { - return other - self - } + public func distance(to other: PythonObject) -> Stride { + return other - self + } - public func advanced(by stride: Stride) -> PythonObject { - return self + stride - } + public func advanced(by stride: Stride) -> PythonObject { + return self + stride + } } extension PythonObject : Equatable, Comparable { - // `Equatable` and `Comparable` are implemented using rich comparison. - // This is consistent with how Python handles comparisons. - private func compared(to other: PythonObject, byOp: Int32) -> Bool { - let lhsObject = ownedPyObject - let rhsObject = other.ownedPyObject - defer { - Py_DecRef(lhsObject) - Py_DecRef(rhsObject) - } - assert(PyErr_Occurred() == nil, - "Python error occurred somewhere but wasn't handled") - switch PyObject_RichCompareBool(lhsObject, rhsObject, byOp) { - case 0: return false - case 1: return true - default: - try! throwPythonErrorIfPresent() - fatalError( - "No result or error returned when comparing \(self) to \(other)") - } - } - - 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) - } - - public static func < (lhs: PythonObject, rhs: PythonObject) -> Bool { - return lhs.compared(to: rhs, byOp: Py_LT) - } - - 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) - } + // `Equatable` and `Comparable` are implemented using rich comparison. + // This is consistent with how Python handles comparisons. + private func compared(to other: PythonObject, byOp: Int32) -> Bool { + let lhsObject = ownedPyObject + let rhsObject = other.ownedPyObject + defer { + Py_DecRef(lhsObject) + Py_DecRef(rhsObject) + } + assert(PyErr_Occurred() == nil, + "Python error occurred somewhere but wasn't handled") + switch PyObject_RichCompareBool(lhsObject, rhsObject, byOp) { + case 0: return false + case 1: return true + default: + try! throwPythonErrorIfPresent() + fatalError("No result or error returned when comparing \(self) to \(other)") + } + } + + 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) + } + + public static func < (lhs: PythonObject, rhs: PythonObject) -> Bool { + return lhs.compared(to: rhs, byOp: Py_LT) + } + + 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) + } +} + +public extension PythonObject { + private func compared(to other: PythonObject, byOp: Int32) -> PythonObject { + let lhsObject = ownedPyObject + let rhsObject = other.ownedPyObject + defer { + Py_DecRef(lhsObject) + Py_DecRef(rhsObject) + } + assert(PyErr_Occurred() == nil, + "Python error occurred somewhere but wasn't handled") + guard let result = PyObject_RichCompare(lhsObject, rhsObject, byOp) else { + // If a Python exception was thrown, throw a corresponding Swift error. + try! throwPythonErrorIfPresent() + fatalError("No result or error returned when comparing \(self) to \(other)") + } + 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) + } } extension PythonObject : Hashable { - public func hash(into hasher: inout Hasher) { - guard let hash = Int(self.__hash__()) else { - fatalError("Cannot use '__hash__' on \(self)") + public func hash(into hasher: inout Hasher) { + guard let hash = Int(self.__hash__()) else { + fatalError("Cannot use '__hash__' on \(self)") + } + hasher.combine(hash) } - hasher.combine(hash) - } } extension PythonObject : MutableCollection { - public typealias Index = PythonObject - public typealias Element = PythonObject - - public var startIndex: Index { - return 0 - } + public typealias Index = PythonObject + public typealias Element = PythonObject - public var endIndex: Index { - return Python.len(self) - } + public var startIndex: Index { + return 0 + } - public func index(after i: Index) -> Index { - return i + PythonObject(1) - } + public var endIndex: Index { + return Python.len(self) + } - public subscript(index: PythonObject) -> PythonObject { - get { - return self[index as PythonConvertible] + public func index(after i: Index) -> Index { + return i + PythonObject(1) } - set { - self[index as PythonConvertible] = newValue + + public subscript(index: PythonObject) -> PythonObject { + get { + return self[index as PythonConvertible] + } + set { + self[index as PythonConvertible] = newValue + } } - } } extension PythonObject : Sequence { - public struct Iterator : IteratorProtocol { - fileprivate let pythonIterator: PythonObject + public struct Iterator : IteratorProtocol { + fileprivate let pythonIterator: PythonObject + + public func next() -> PythonObject? { + guard let result = PyIter_Next(self.pythonIterator.borrowedPyObject) else { + try! throwPythonErrorIfPresent() + return nil + } + return PythonObject(consuming: result) + } + } - public func next() -> PythonObject? { - guard let result = PyIter_Next(self.pythonIterator.borrowedPyObject) else { - try! throwPythonErrorIfPresent() - return nil - } - return PythonObject(consuming: result) + public func makeIterator() -> Iterator { + guard let result = PyObject_GetIter(borrowedPyObject) else { + try! throwPythonErrorIfPresent() + // Unreachable. A Python `TypeError` must have been thrown. + preconditionFailure() + } + return Iterator(pythonIterator: PythonObject(consuming: result)) } - } +} - public func makeIterator() -> Iterator { - guard let result = PyObject_GetIter(borrowedPyObject) else { - try! throwPythonErrorIfPresent() - // Unreachable. A Python `TypeError` must have been thrown. - preconditionFailure() +extension PythonObject { + public var count: Int { + checking.count! } - return Iterator(pythonIterator: PythonObject(consuming: result)) - } } //===----------------------------------------------------------------------===// // `ExpressibleByLiteral` conformances //===----------------------------------------------------------------------===// -extension PythonObject : ExpressibleByBooleanLiteral, - ExpressibleByIntegerLiteral, - ExpressibleByFloatLiteral, - ExpressibleByStringLiteral { - public init(booleanLiteral value: Bool) { - self.init(value) - } - public init(integerLiteral value: Int) { - self.init(value) - } - public init(floatLiteral value: Double) { - self.init(value) - } - public init(stringLiteral value: String) { - self.init(value) - } -} - -extension PythonObject : ExpressibleByArrayLiteral, - ExpressibleByDictionaryLiteral { - public init(arrayLiteral elements: PythonObject...) { - self.init(elements) - } - public typealias Key = PythonObject - public typealias Value = PythonObject - public init(dictionaryLiteral elements: (PythonObject, PythonObject)...) { - self.init(Dictionary(elements, uniquingKeysWith: { lhs, _ in lhs })) - } +extension PythonObject : ExpressibleByBooleanLiteral, ExpressibleByIntegerLiteral, +ExpressibleByFloatLiteral, ExpressibleByStringLiteral { + public init(booleanLiteral value: Bool) { + self.init(value) + } + public init(integerLiteral value: Int) { + self.init(value) + } + public init(floatLiteral value: Double) { + self.init(value) + } + public init(stringLiteral value: String) { + self.init(value) + } +} + +extension PythonObject : ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral { + public init(arrayLiteral elements: PythonObject...) { + self.init(elements) + } + 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)...) { + _ = 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) + } +} + +public struct PythonBytes : PythonConvertible, ConvertibleFromPython, Hashable { + public private(set) var pythonObject: PythonObject + + public init?(_ pythonObject: PythonObject) { + // We try to get the string/size pointers out. If it works, hooray, this is a bytes + // otherwise it isn't. + let pyObject = pythonObject.ownedPyObject + defer { Py_DecRef(pyObject) } + + var length = 0 + var buffer: UnsafeMutablePointer? = nil + + switch PyBytes_AsStringAndSize(pyObject, &buffer, &length) { + case 0: + self.pythonObject = pythonObject + default: + return nil + } + } + + @inlinable + public init(_ bytes: Bytes) where Bytes.Element == UInt8 { + let possibleSelf = bytes.withContiguousStorageIfAvailable { storagePtr in + PythonBytes.fromBytePointer(storagePtr) + } + if let actualSelf = possibleSelf { + self = actualSelf + } else { + let temporaryBuffer = Array(bytes) + self = temporaryBuffer.withUnsafeBufferPointer { + PythonBytes.fromBytePointer($0) + } + } + } + + @inlinable + public init(_ bytes: Bytes) where Bytes.Element == Int8 { + let possibleSelf = bytes.withContiguousStorageIfAvailable { storagePtr in + PythonBytes.fromBytePointer(storagePtr) + } + if let actualSelf = possibleSelf { + self = actualSelf + } else { + let temporaryBuffer = Array(bytes) + self = temporaryBuffer.withUnsafeBufferPointer { + PythonBytes.fromBytePointer($0) + } + } + } + + private init(bytesObject: PythonObject) { + self.pythonObject = bytesObject + } + + @usableFromInline + static func fromBytePointer(_ bytes: UnsafeBufferPointer) -> PythonBytes { + bytes.withMemoryRebound(to: Int8.self) { reboundPtr in + PythonBytes.fromBytePointer(reboundPtr) + } + } + + @usableFromInline + static func fromBytePointer(_ bytes: UnsafeBufferPointer) -> PythonBytes { + let v = PyBytes_FromStringAndSize(bytes.baseAddress, bytes.count)! + return PythonBytes(bytesObject: PythonObject(consuming: v)) + } + + public func withUnsafeBytes( + _ callback: (UnsafeRawBufferPointer) throws -> ReturnValue + ) rethrows -> ReturnValue { + let pyObject = self.pythonObject.ownedPyObject + defer { Py_DecRef(pyObject) } + + var length = 0 + var buffer: UnsafeMutablePointer? = nil + + switch PyBytes_AsStringAndSize(pyObject, &buffer, &length) { + case 0: + let buffer = UnsafeRawBufferPointer(start: buffer, count: length) + return try callback(buffer) + default: + try! throwPythonErrorIfPresent() + fatalError("No result or error getting interior buffer for bytes \(self)") + } + } +} + +//===----------------------------------------------------------------------===// +// PythonFunction - create functions in Swift that can be called from Python +//===----------------------------------------------------------------------===// + +/// Create functions in Swift that can be called from Python +/// +/// Example: +/// +/// The Python code `map(lambda(x: x * 2), [10, 12, 14])` would be written as: +/// +/// Python.map(PythonFunction { x in x * 2 }, [10, 12, 14]) // [20, 24, 28] +/// +final class PyFunction { + enum CallingConvention { + 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) + return try callSwiftFunction(argumentsTuple, keywordArguments) + } +} + +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 + /// mutated, so the next best solution is to use `[KeyValuePairs.Element]`. + public init(_ fn: @escaping ([PythonObject], [(key: String, value: PythonObject)]) throws -> PythonConvertible) { + function = PyFunction { argumentsAsTuple, keywordArgumentsAsDictionary in + var kwargs: [(String, PythonObject)] = [] + for keyAndValue in keywordArgumentsAsDictionary.items() { + let (key, value) = keyAndValue.tuple2 + kwargs.append((String(key)!, value)) + } + return try fn(argumentsAsTuple.map { $0 }, kwargs) + } + } +} + +extension PythonFunction : PythonConvertible { + public var pythonObject: PythonObject { + // Ensure Python is initialized, and check for version match. + let versionMajor = Python.versionInfo.major + let versionMinor = Python.versionInfo.minor + guard (versionMajor == 3 && versionMinor >= 1) || versionMajor > 3 else { + fatalError("PythonFunction only supports Python 3.1 and above.") + } + + let destructor: @convention(c) (PyObjectPointer?) -> Void = { capsulePointer in + let funcPointer = PyCapsule_GetPointer(capsulePointer, nil) + Unmanaged.fromOpaque(funcPointer).release() + } + let funcPointer = Unmanaged.passRetained(function).toOpaque() + let capsulePointer = PyCapsule_New( + funcPointer, + nil, + unsafeBitCast(destructor, to: OpaquePointer.self) + ) + + var methodDefinition: UnsafeMutablePointer + switch function.callingConvention { + case .varArgs: + methodDefinition = PythonFunction.sharedMethodDefinition + case .varArgsWithKeywords: + methodDefinition = PythonFunction.sharedMethodWithKeywordsDefinition + } + let pyFuncPointer = PyCFunction_NewEx( + methodDefinition, + capsulePointer, + nil + ) + + return PythonObject(consuming: pyFuncPointer) + } +} + +fileprivate extension PythonFunction { + static let sharedMethodDefinition: UnsafeMutablePointer = { + let name: StaticString = "pythonkit_swift_function" + // `utf8Start` is a property of StaticString, thus, it has a stable pointer. + let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) + + let methodImplementationPointer = unsafeBitCast( + PythonFunction.sharedMethodImplementation, to: OpaquePointer.self) + + /// The standard calling convention. See Python C API docs + let METH_VARARGS = 0x0001 as Int32 + + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = PyMethodDef( + ml_name: namePointer, + ml_meth: methodImplementationPointer, + ml_flags: METH_VARARGS, + ml_doc: nil + ) + + 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. + let namePointer = UnsafeRawPointer(name.utf8Start).assumingMemoryBound(to: Int8.self) + + let methodImplementationPointer = unsafeBitCast( + PythonFunction.sharedMethodWithKeywordsImplementation, to: OpaquePointer.self) + + /// A combination of flags that supports `**kwargs`. See Python C API docs + let METH_VARARGS = 0x0001 as Int32 + let METH_KEYWORDS = 0x0002 as Int32 + + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = PyMethodDef( + ml_name: namePointer, + ml_meth: methodImplementationPointer, + ml_flags: METH_VARARGS | METH_KEYWORDS, + ml_doc: nil + ) + + return pointer + }() + + private static let sharedMethodImplementation: @convention(c) ( + PyObjectPointer?, PyObjectPointer? + ) -> PyObjectPointer? = { context, argumentsPointer in + guard let argumentsPointer = argumentsPointer, let capsulePointer = context else { + return nil + } + + let funcPointer = PyCapsule_GetPointer(capsulePointer, nil) + let function = Unmanaged.fromOpaque(funcPointer).takeUnretainedValue() + + do { + let argumentsAsTuple = PythonObject(consuming: argumentsPointer) + return try function(argumentsAsTuple).ownedPyObject + } catch { + PythonFunction.setPythonError(swiftError: error) + 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 + guard let argumentsPointer = argumentsPointer, let capsulePointer = context else { + return nil + } + + let funcPointer = PyCapsule_GetPointer(capsulePointer, nil) + let function = Unmanaged.fromOpaque(funcPointer).takeUnretainedValue() + + do { + let argumentsAsTuple = PythonObject(consuming: argumentsPointer) + var keywordArgumentsAsDictionary: PythonObject + if let keywordArgumentsPointer = keywordArgumentsPointer { + keywordArgumentsAsDictionary = PythonObject(consuming: keywordArgumentsPointer) + } else { + keywordArgumentsAsDictionary = [:] + } + return try function(argumentsAsTuple, keywordArgumentsAsDictionary).ownedPyObject + } catch { + PythonFunction.setPythonError(swiftError: error) + return nil // This must only be `nil` if an exception has been set + } + } + + private static func setPythonError(swiftError: Error) { + if let pythonObject = swiftError as? PythonObject { + if Bool(Python.isinstance(pythonObject, Python.BaseException))! { + // We are an instance of an Exception class type. Set the exception class to the object's type: + PyErr_SetString(Python.type(pythonObject).ownedPyObject, pythonObject.description) + } else { + // Assume an actual class type was thrown (rather than an instance) + // Crashes if it was neither a subclass of BaseException nor an instance of one. + // + // We *could* check to see whether `pythonObject` is a class here and fall back + // to the default case of setting a generic Exception, below, but we also want + // people to write valid code. + PyErr_SetString(pythonObject.ownedPyObject, pythonObject.description) + } + } else { + // Make a generic Python Exception based on the Swift Error: + PyErr_SetString(Python.Exception.ownedPyObject, "\(type(of: swiftError)) raised in Swift: \(swiftError)") + } + } +} + +extension PythonObject: Error {} + +// From Python's C Headers: +struct PyMethodDef { + /// The name of the built-in function/method + var ml_name: UnsafePointer + + /// The C function that implements it. + /// Since this accepts multiple function signatures, the Swift type must be opaque here. + var ml_meth: OpaquePointer + + /// Combination of METH_xxx flags, which mostly describe the args expected by the C func + var ml_flags: Int32 + + /// The __doc__ attribute, or NULL + var ml_doc: UnsafePointer? +} + +//===----------------------------------------------------------------------===// +// PythonInstanceMethod - create functions that can be bound to a Python object +//===----------------------------------------------------------------------===// + +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) + } +} + +extension PythonInstanceMethod : PythonConvertible { + public var pythonObject: PythonObject { + let pyFuncPointer = function.pythonObject.ownedPyObject + let methodPointer = PyInstanceMethod_New(pyFuncPointer) + return PythonObject(consuming: methodPointer) + } +} + +//===----------------------------------------------------------------------===// +// PythonClass - construct subclasses of a Python class +//===----------------------------------------------------------------------===// + +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: { _, _ 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) + } +} + +extension PythonClass : PythonConvertible { + public var pythonObject: PythonObject { + typeObject + } } diff --git a/PythonKit/PythonLibrary+Symbols.swift b/PythonKit/PythonLibrary+Symbols.swift index d3dab7c..c4db681 100644 --- a/PythonKit/PythonLibrary+Symbols.swift +++ b/PythonKit/PythonLibrary+Symbols.swift @@ -15,13 +15,17 @@ //===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===// -// Required Python typealias and constants. +// Required Python typealiases and constants. //===----------------------------------------------------------------------===// +@usableFromInline typealias PyObjectPointer = UnsafeMutableRawPointer +typealias PyMethodDefPointer = UnsafeMutableRawPointer typealias PyCCharPointer = UnsafePointer typealias PyBinaryOperation = - @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? + @convention(c) (PyObjectPointer?, PyObjectPointer?) -> PyObjectPointer? +typealias PyUnaryOperation = + @convention(c) (PyObjectPointer?) -> PyObjectPointer? let Py_LT: Int32 = 0 let Py_LE: Int32 = 1 @@ -35,181 +39,248 @@ let Py_GE: Int32 = 5 //===----------------------------------------------------------------------===// let Py_Initialize: @convention(c) () -> Void = - PythonLibrary.loadSymbol(name: "Py_Initialize") + PythonLibrary.loadSymbol(name: "Py_Initialize") let Py_IncRef: @convention(c) (PyObjectPointer?) -> Void = - PythonLibrary.loadSymbol(name: "Py_IncRef") + PythonLibrary.loadSymbol(name: "Py_IncRef") let Py_DecRef: @convention(c) (PyObjectPointer?) -> Void = - PythonLibrary.loadSymbol(name: "Py_DecRef") + PythonLibrary.loadSymbol(name: "Py_DecRef") let PyImport_ImportModule: @convention(c) ( - PyCCharPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyImport_ImportModule") + PyCCharPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyImport_ImportModule") let PyEval_GetBuiltins: @convention(c) () -> PyObjectPointer = - PythonLibrary.loadSymbol(name: "PyEval_GetBuiltins") + PythonLibrary.loadSymbol(name: "PyEval_GetBuiltins") let PyRun_SimpleString: @convention(c) (PyCCharPointer) -> Void = PythonLibrary.loadSymbol(name: "PyRun_SimpleString") +let PyCFunction_NewEx: @convention(c) (PyMethodDefPointer, UnsafeMutableRawPointer, UnsafeMutableRawPointer?) -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyCFunction_NewEx") + +let PyInstanceMethod_New: @convention(c) (PyObjectPointer) -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyInstanceMethod_New") + +/// The last argument would ideally be of type `@convention(c) (PyObjectPointer?) -> Void`. +/// Due to SR-15871 and the source-breaking nature of potential workarounds, the +/// static typing was removed. The caller must now manually cast a closure to +/// `OpaquePointer` before passing it into `PyCapsule_New`. +let PyCapsule_New: @convention(c) ( + UnsafeMutableRawPointer, UnsafePointer?, + OpaquePointer) -> PyObjectPointer = + PythonLibrary.loadSymbol(name: "PyCapsule_New") + +let PyCapsule_GetPointer: @convention(c) (PyObjectPointer?, UnsafePointer?) -> UnsafeMutableRawPointer = + PythonLibrary.loadSymbol(name: "PyCapsule_GetPointer") + +let PyErr_SetString: @convention(c) (PyObjectPointer, UnsafePointer?) -> Void = + PythonLibrary.loadSymbol(name: "PyErr_SetString") + let PyErr_Occurred: @convention(c) () -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyErr_Occurred") + PythonLibrary.loadSymbol(name: "PyErr_Occurred") let PyErr_Clear: @convention(c) () -> Void = - PythonLibrary.loadSymbol(name: "PyErr_Clear") + PythonLibrary.loadSymbol(name: "PyErr_Clear") let PyErr_Fetch: @convention(c) ( - UnsafeMutablePointer, - UnsafeMutablePointer, - UnsafeMutablePointer) -> Void = - PythonLibrary.loadSymbol(name: "PyErr_Fetch") + UnsafeMutablePointer, + UnsafeMutablePointer, + UnsafeMutablePointer) -> Void = + PythonLibrary.loadSymbol(name: "PyErr_Fetch") let PyDict_New: @convention(c) () -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyDict_New") + 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") + PyObjectPointer?, PyObjectPointer, PyObjectPointer) -> Void = + PythonLibrary.loadSymbol(name: "PyDict_SetItem") let PyObject_GetItem: @convention(c) ( - PyObjectPointer, PyObjectPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyObject_GetItem") + PyObjectPointer, PyObjectPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_GetItem") let PyObject_SetItem: @convention(c) ( - PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Void = - PythonLibrary.loadSymbol(name: "PyObject_SetItem") + PyObjectPointer, PyObjectPointer, PyObjectPointer) -> Void = + PythonLibrary.loadSymbol(name: "PyObject_SetItem") let PyObject_DelItem: @convention(c) ( - PyObjectPointer, PyObjectPointer) -> Void = - PythonLibrary.loadSymbol(name: "PyObject_DelItem") + PyObjectPointer, PyObjectPointer) -> Void = + PythonLibrary.loadSymbol(name: "PyObject_DelItem") let PyObject_Call: @convention(c) ( - PyObjectPointer, PyObjectPointer, - PyObjectPointer?) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyObject_Call") + PyObjectPointer, PyObjectPointer, + PyObjectPointer?) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_Call") let PyObject_CallObject: @convention(c) ( - PyObjectPointer, PyObjectPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyObject_CallObject") + PyObjectPointer, PyObjectPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_CallObject") let PyObject_GetAttrString: @convention(c) ( - PyObjectPointer, PyCCharPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyObject_GetAttrString") + PyObjectPointer, PyCCharPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_GetAttrString") let PyObject_SetAttrString: @convention(c) ( - PyObjectPointer, PyCCharPointer, PyObjectPointer) -> Int32 = - PythonLibrary.loadSymbol(name: "PyObject_SetAttrString") + PyObjectPointer, PyCCharPointer, PyObjectPointer) -> Int32 = + PythonLibrary.loadSymbol(name: "PyObject_SetAttrString") let PySlice_New: @convention(c) ( - PyObjectPointer?, PyObjectPointer?, - PyObjectPointer?) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PySlice_New") + PyObjectPointer?, PyObjectPointer?, + PyObjectPointer?) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PySlice_New") let PyTuple_New: @convention(c) (Int) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyTuple_New") + PythonLibrary.loadSymbol(name: "PyTuple_New") let PyTuple_SetItem: @convention(c) ( - PyObjectPointer, Int, PyObjectPointer) -> Void = - PythonLibrary.loadSymbol(name: "PyTuple_SetItem") + PyObjectPointer, Int, PyObjectPointer) -> Void = + PythonLibrary.loadSymbol(name: "PyTuple_SetItem") + +let PyObject_RichCompare: @convention(c) ( + PyObjectPointer, PyObjectPointer, Int32) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_RichCompare") let PyObject_RichCompareBool: @convention(c) ( - PyObjectPointer, PyObjectPointer, Int32) -> Int32 = - PythonLibrary.loadSymbol(name: "PyObject_RichCompareBool") + PyObjectPointer, PyObjectPointer, Int32) -> Int32 = + PythonLibrary.loadSymbol(name: "PyObject_RichCompareBool") let PyDict_Next: @convention(c) ( - PyObjectPointer, UnsafeMutablePointer, - UnsafeMutablePointer, - UnsafeMutablePointer) -> Int32 = - PythonLibrary.loadSymbol(name: "PyDict_Next") + PyObjectPointer, UnsafeMutablePointer, + UnsafeMutablePointer, + UnsafeMutablePointer) -> Int32 = + PythonLibrary.loadSymbol(name: "PyDict_Next") let PyIter_Next: @convention(c) ( - PyObjectPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyIter_Next") + PyObjectPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyIter_Next") let PyObject_GetIter: @convention(c) ( - PyObjectPointer) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyObject_GetIter") + PyObjectPointer) -> PyObjectPointer? = + PythonLibrary.loadSymbol(name: "PyObject_GetIter") let PyList_New: @convention(c) (Int) -> PyObjectPointer? = - PythonLibrary.loadSymbol(name: "PyList_New") + PythonLibrary.loadSymbol(name: "PyList_New") let PyList_SetItem: @convention(c) ( - PyObjectPointer, Int, PyObjectPointer) -> Int32 = - PythonLibrary.loadSymbol(name: "PyList_SetItem") + PyObjectPointer, Int, PyObjectPointer) -> Int32 = + PythonLibrary.loadSymbol(name: "PyList_SetItem") let PyBool_FromLong: @convention(c) (Int) -> PyObjectPointer = - PythonLibrary.loadSymbol(name: "PyBool_FromLong") + PythonLibrary.loadSymbol(name: "PyBool_FromLong") let PyFloat_AsDouble: @convention(c) (PyObjectPointer) -> Double = - PythonLibrary.loadSymbol(name: "PyFloat_AsDouble") + PythonLibrary.loadSymbol(name: "PyFloat_AsDouble") let PyFloat_FromDouble: @convention(c) (Double) -> PyObjectPointer = - PythonLibrary.loadSymbol(name: "PyFloat_FromDouble") + PythonLibrary.loadSymbol(name: "PyFloat_FromDouble") let PyInt_AsLong: @convention(c) (PyObjectPointer) -> Int = - PythonLibrary.loadSymbol( - name: "PyLong_AsLong", - legacyName: "PyInt_AsLong") + PythonLibrary.loadSymbol( + name: "PyLong_AsLong", + legacyName: "PyInt_AsLong") let PyInt_FromLong: @convention(c) (Int) -> PyObjectPointer = - PythonLibrary.loadSymbol( - name: "PyLong_FromLong", - legacyName: "PyInt_FromLong") + PythonLibrary.loadSymbol( + name: "PyLong_FromLong", + legacyName: "PyInt_FromLong") let PyInt_AsUnsignedLongMask: @convention(c) (PyObjectPointer) -> UInt = - PythonLibrary.loadSymbol( - name: "PyLong_AsUnsignedLongMask", - legacyName: "PyInt_AsUnsignedLongMask") + PythonLibrary.loadSymbol( + name: "PyLong_AsUnsignedLongMask", + legacyName: "PyInt_AsUnsignedLongMask") let PyInt_FromSize_t: @convention(c) (UInt) -> PyObjectPointer = - PythonLibrary.loadSymbol( - name: "PyLong_FromUnsignedLong", - legacyName: "PyInt_FromSize_t") + PythonLibrary.loadSymbol( + name: "PyLong_FromUnsignedLong", + legacyName: "PyInt_FromSize_t") let PyString_AsString: @convention(c) (PyObjectPointer) -> PyCCharPointer? = - PythonLibrary.loadSymbol( - name: "PyUnicode_AsUTF8", - legacyName: "PyString_AsString") + PythonLibrary.loadSymbol( + name: "PyUnicode_AsUTF8", + legacyName: "PyString_AsString") let PyString_FromStringAndSize: @convention(c) ( - PyCCharPointer?, Int) -> (PyObjectPointer?) = - PythonLibrary.loadSymbol( - name: "PyUnicode_DecodeUTF8", - legacyName: "PyString_FromStringAndSize") + PyCCharPointer?, Int) -> (PyObjectPointer?) = + PythonLibrary.loadSymbol( + name: "PyUnicode_DecodeUTF8", + legacyName: "PyString_FromStringAndSize") + +let PyBytes_FromStringAndSize: @convention(c) ( + PyCCharPointer?, Int) -> (PyObjectPointer?) = + PythonLibrary.loadSymbol( + name: "PyBytes_FromStringAndSize", + legacyName: "PyString_FromStringAndSize") + +let PyBytes_AsStringAndSize: @convention(c) ( + PyObjectPointer, + UnsafeMutablePointer?>?, + UnsafeMutablePointer?) -> CInt = + PythonLibrary.loadSymbol( + name: "PyBytes_AsStringAndSize", + legacyName: "PyString_AsStringAndSize") let _Py_ZeroStruct: PyObjectPointer = - PythonLibrary.loadSymbol(name: "_Py_ZeroStruct") + PythonLibrary.loadSymbol(name: "_Py_ZeroStruct") let _Py_TrueStruct: PyObjectPointer = - PythonLibrary.loadSymbol(name: "_Py_TrueStruct") + PythonLibrary.loadSymbol(name: "_Py_TrueStruct") let PyBool_Type: PyObjectPointer = - PythonLibrary.loadSymbol(name: "PyBool_Type") + PythonLibrary.loadSymbol(name: "PyBool_Type") let PySlice_Type: PyObjectPointer = - PythonLibrary.loadSymbol(name: "PySlice_Type") + PythonLibrary.loadSymbol(name: "PySlice_Type") let PyNumber_Add: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_Add") + PythonLibrary.loadSymbol(name: "PyNumber_Add") let PyNumber_Subtract: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_Subtract") + PythonLibrary.loadSymbol(name: "PyNumber_Subtract") let PyNumber_Multiply: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_Multiply") + PythonLibrary.loadSymbol(name: "PyNumber_Multiply") let PyNumber_TrueDivide: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_TrueDivide") + PythonLibrary.loadSymbol(name: "PyNumber_TrueDivide") let PyNumber_InPlaceAdd: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_InPlaceAdd") + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceAdd") let PyNumber_InPlaceSubtract: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_InPlaceSubtract") + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceSubtract") let PyNumber_InPlaceMultiply: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_InPlaceMultiply") + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceMultiply") let PyNumber_InPlaceTrueDivide: PyBinaryOperation = - PythonLibrary.loadSymbol(name: "PyNumber_InPlaceTrueDivide") + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceTrueDivide") + +let PyNumber_Negative: PyUnaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_Negative") + +let PyNumber_And: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_And") + +let PyNumber_Or: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_Or") + +let PyNumber_Xor: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_Xor") + +let PyNumber_InPlaceAnd: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceAnd") + +let PyNumber_InPlaceOr: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceOr") + +let PyNumber_InPlaceXor: PyBinaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_InPlaceXor") + +let PyNumber_Invert: PyUnaryOperation = + PythonLibrary.loadSymbol(name: "PyNumber_Invert") diff --git a/PythonKit/PythonLibrary.swift b/PythonKit/PythonLibrary.swift index 6f81b9c..ab94a3f 100644 --- a/PythonKit/PythonLibrary.swift +++ b/PythonKit/PythonLibrary.swift @@ -18,8 +18,10 @@ import Darwin #elseif canImport(Glibc) import Glibc +#elseif canImport(Musl) +import Musl #elseif os(Windows) -import MSVCRT +import CRT import WinSDK #endif @@ -28,226 +30,287 @@ import WinSDK //===----------------------------------------------------------------------===// public struct PythonLibrary { - private static let shared = PythonLibrary() - private static let pythonLegacySymbolName = "PyString_AsString" - private static var librarySymbolsLoaded = false - - private let pythonLibraryHandle: UnsafeMutableRawPointer - private let isLegacyPython: Bool - - private init() { - guard let pythonLibraryHandle = PythonLibrary.loadPythonLibrary() else { - fatalError(""" - Python library not found. Set the \(Environment.library.key) \ - environment variable with the path to a Python library. - """) + public enum Error: Swift.Error, Equatable, CustomStringConvertible { + case pythonLibraryNotFound + + public var description: String { + switch self { + case .pythonLibraryNotFound: + return """ + Python library not found. Set the \(Environment.library.key) \ + environment variable with the path to a Python library. + """ + } + } } - self.pythonLibraryHandle = pythonLibraryHandle - - // Check if Python is legacy (Python 2) - isLegacyPython = PythonLibrary.loadSymbol( - pythonLibraryHandle, - PythonLibrary.pythonLegacySymbolName) != nil - if isLegacyPython { - PythonLibrary.log( - "Loaded legacy Python library, using legacy symbols...") + + private static let pythonInitializeSymbolName = "Py_Initialize" + private static let pythonLegacySymbolName = "PyString_AsString" + +#if canImport(Darwin) + private static let defaultLibraryHandle = UnsafeMutableRawPointer(bitPattern: -2) // RTLD_DEFAULT +#else + private static let defaultLibraryHandle: UnsafeMutableRawPointer? = nil // RTLD_DEFAULT +#endif + + private static var isPythonLibraryLoaded = false + private static var _pythonLibraryHandle: UnsafeMutableRawPointer? + private static var pythonLibraryHandle: UnsafeMutableRawPointer? { + try! PythonLibrary.loadLibrary() + return self._pythonLibraryHandle } - PythonLibrary.librarySymbolsLoaded = true - } - - static func loadSymbol( - _ libraryHandle: UnsafeMutableRawPointer, _ name: String - ) -> UnsafeMutableRawPointer? { - #if canImport(Darwin) || canImport(Glibc) - return dlsym(libraryHandle, name) - #elseif os(Windows) - let moduleHandle = libraryHandle - .assumingMemoryBound(to: HINSTANCE__.self) - let moduleSymbol = GetProcAddress(moduleHandle, name) - return unsafeBitCast(moduleSymbol, to: UnsafeMutableRawPointer?.self) - #endif - } - - static func loadSymbol( - name: String, legacyName: String? = nil, type: T.Type = T.self - ) -> T { - var name = name - if let legacyName = legacyName, PythonLibrary.shared.isLegacyPython { - name = legacyName + + /// 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 } + let pythonLibraryHandle = self.loadPythonLibrary() + guard self.isPythonLibraryLoaded(at: pythonLibraryHandle) else { + throw Error.pythonLibraryNotFound + } + self.isPythonLibraryLoaded = true + self._pythonLibraryHandle = pythonLibraryHandle } - log("Loading symbol '\(name)' from the Python library...") - return unsafeBitCast( - loadSymbol(PythonLibrary.shared.pythonLibraryHandle, name), - to: type - ) - } -} + private static let isLegacyPython: Bool = { + let isLegacyPython = PythonLibrary.loadSymbol(PythonLibrary.pythonLibraryHandle, PythonLibrary.pythonLegacySymbolName) != nil + if isLegacyPython { + PythonLibrary.log("Loaded legacy Python library, using legacy symbols...") + } + 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 + } -// Methods of `PythonLibrary` required to set a given Python version. -public extension PythonLibrary { - static func useVersion(_ major: Int, _ minor: Int? = nil) { - precondition(!librarySymbolsLoaded, """ - Error: \(#function) should not be called after any Python library \ - has already been loaded. - """) - let version = PythonVersion(major: major, minor: minor) - PythonLibrary.Environment.version.set(version.versionString) - } + log("Loading symbol '\(name)' from the Python library...") + return unsafeBitCast(self.loadSymbol(self.pythonLibraryHandle, name), to: type) + } } -// `PythonVersion` struct that defines a given Python version. -private extension PythonLibrary { - struct PythonVersion { - let major: Int - let minor: Int? +// 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() - static let versionSeparator: Character = "." + private static let libraryPathVersionCharacter: Character = ":" - init(major: Int, minor: Int?) { - self.major = major - self.minor = minor - } +#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) + private static var libraryNames = ["libpython:", "libpython:m"] + private static var libraryPathExtensions = [".so"] + private static var librarySearchPaths = [""] + private static var libraryVersionSeparator = "." +#elseif os(Windows) + private static var libraryNames = ["python:"] + private static var libraryPathExtensions = [".dll"] + private static var librarySearchPaths = [""] + private static var libraryVersionSeparator = "" +#endif - var versionString: String { - var versionString = String(major) - if let minor = minor { - versionString += "\(PythonVersion.versionSeparator)\(minor)" - } - return versionString - } - } -} + 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 + libraryPaths.append(libraryPath) + } + } + } + return libraryPaths + }() -// `PythonLibrary.Environment` enum used to read and set environment variables. -private extension PythonLibrary { - enum Environment: String { - private static let keyPrefix = "PYTHON" - private static let keySeparator = "_" + private static func loadSymbol( + _ libraryHandle: UnsafeMutableRawPointer?, _ name: String) -> UnsafeMutableRawPointer? { +#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 + } - case library = "LIBRARY" - case version = "VERSION" - case loaderLogging = "LOADER_LOGGING" + private static func isPythonLibraryLoaded(at pythonLibraryHandle: UnsafeMutableRawPointer? = nil) -> Bool { + let pythonLibraryHandle = pythonLibraryHandle ?? self.defaultLibraryHandle + return self.loadSymbol(pythonLibraryHandle, self.pythonInitializeSymbolName) != nil + } - var key: String { - return Environment.keyPrefix + Environment.keySeparator + rawValue + private static func loadPythonLibrary() -> UnsafeMutableRawPointer? { + let pythonLibraryHandle: UnsafeMutableRawPointer? + if self.isPythonLibraryLoaded() { + pythonLibraryHandle = self.defaultLibraryHandle + } + else if let pythonLibraryPath = Environment.library.value { + pythonLibraryHandle = self.loadPythonLibrary(at: pythonLibraryPath) + } + else { + pythonLibraryHandle = self.findAndLoadExternalPythonLibrary() + } + return pythonLibraryHandle } - var value: String? { - guard let value = getenv(key) else { return nil } - return String(cString: value) + private static func findAndLoadExternalPythonLibrary() -> UnsafeMutableRawPointer? { + for majorVersion in supportedMajorVersions { + for minorVersion in supportedMinorVersions { + for libraryPath in libraryPaths { + let version = PythonVersion(major: majorVersion, minor: minorVersion) + guard let pythonLibraryHandle = loadPythonLibrary( + at: libraryPath, version: version) else { + continue + } + return pythonLibraryHandle + } + } + } + return nil } - func set(_ value: String) { - #if canImport(Darwin) || canImport(Glibc) - setenv(key, value, 1) - #elseif os(Windows) - _putenv_s(key, value) - #endif + 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 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 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) + 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.") + } + return pythonLibraryHandle } - } } -// Methods of `PythonLibrary` required to load the Python library. -private extension PythonLibrary { - static let supportedMajorVersions: [Int] = [3, 2] - static let supportedMinorVersions: [Int] = Array(0...30).reversed() - - static let libraryPathVersionCharacter: Character = ":" - - #if canImport(Darwin) - static var libraryNames = ["Python.framework/Versions/:/Python"] - static var libraryPathExtensions = [""] - static var librarySearchPaths = ["", "/usr/local/Frameworks/"] - static var libraryVersionSeparator = "." - #elseif os(Linux) - static var libraryNames = ["libpython:", "libpython:m"] - static var libraryPathExtensions = [".so"] - static var librarySearchPaths = [""] - static var libraryVersionSeparator = "." - #elseif os(Windows) - static var libraryNames = ["python:"] - static var libraryPathExtensions = [".dll"] - static var librarySearchPaths = [""] - static var libraryVersionSeparator = "" - #endif - - static let libraryPaths: [String] = { - var libraryPaths: [String] = [] - for librarySearchPath in librarySearchPaths { - for libraryName in libraryNames { - for libraryPathExtension in libraryPathExtensions { - let libraryPath = - librarySearchPath + libraryName + libraryPathExtension - libraryPaths.append(libraryPath) - } - } +// Methods of `PythonLibrary` required to set a given Python version or library path. +extension PythonLibrary { + private static func enforceNonLoadedPythonLibrary(function: String = #function) { + precondition(!self.isPythonLibraryLoaded, """ + Error: \(function) should not be called after any Python library \ + has already been loaded. + """) } - return libraryPaths - }() - static func loadPythonLibrary() -> UnsafeMutableRawPointer? { - if let pythonLibraryPath = Environment.library.value { - return loadPythonLibrary(at: pythonLibraryPath) + /// Use the Python library with the specified version. + /// - Parameters: + /// - major: Major version or nil to use any Python version. + /// - minor: Minor version or nil to use any minor version. + public static func useVersion(_ major: Int?, _ minor: Int? = nil) { + self.enforceNonLoadedPythonLibrary() + let version = PythonVersion(major: major, minor: minor) + PythonLibrary.Environment.version.set(version.versionString) } - for majorVersion in supportedMajorVersions { - for minorVersion in supportedMinorVersions { - for libraryPath in libraryPaths { - let version = PythonVersion(major: majorVersion, minor: minorVersion) - guard let pythonLibraryHandle = loadPythonLibrary( - at: libraryPath, version: version) else { - continue - } - return pythonLibraryHandle - } - } + /// 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?) { + self.enforceNonLoadedPythonLibrary() + PythonLibrary.Environment.library.set(path ?? "") } - return nil - } - - 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 - } +} + +// `PythonVersion` struct that defines a given Python version. +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 \ + without the major version. + """) + self.major = major + self.minor = minor + } + + var versionString: String { + guard let major = major else { return "" } + var versionString = String(major) + if let minor = minor { + versionString += "\(PythonVersion.versionSeparator)\(minor)" + } + return versionString + } } +} + +// `PythonLibrary.Environment` enum used to read and set environment variables. +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" - let libraryVersionString = versionString - .split(separator: PythonVersion.versionSeparator) - .joined(separator: libraryVersionSeparator) - let path = path.split(separator: libraryPathVersionCharacter) - .joined(separator: libraryVersionString) - return loadPythonLibrary(at: path) - } - - static func loadPythonLibrary(at path: String) -> UnsafeMutableRawPointer? { - log("Trying to load library at '\(path)'...") - #if canImport(Darwin) || canImport(Glibc) - // 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 { - log("Library at '\(path)' was sucessfully loaded.") + 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 os(Windows) + _putenv_s(key, value) +#else + setenv(key, value, 1) +#endif + } } - return pythonLibraryHandle - } } // Methods of `PythonLibrary` used for logging messages. -private extension PythonLibrary { - static func log(_ message: String) { - guard Environment.loaderLogging.value != nil else { return } - fputs(message + "\n", stderr) - } +extension PythonLibrary { + private static func log(_ message: String) { + guard Environment.loaderLogging.value != nil else { return } + fputs(message + "\n", stderr) + } } diff --git a/PythonTool/Info.plist b/PythonTool/Info.plist deleted file mode 100644 index 4a83cc1..0000000 --- a/PythonTool/Info.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - NSHumanReadableCopyright - Copyright © 2017 Pedro José Pereira Vieito. - - diff --git a/PythonTool/OperatingSystemVersion.swift b/PythonTool/OperatingSystemVersion.swift deleted file mode 100644 index 26c852d..0000000 --- a/PythonTool/OperatingSystemVersion.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// OperatingSystemVersion.swift -// PythonTool -// -// Created by Pedro José Pereira Vieito on 29/1/18. -// Copyright © 2018 Pedro José Pereira Vieito. All rights reserved. -// - -import Foundation - -extension OperatingSystemVersion: CustomStringConvertible { - - public var shortVersion: String { - return "\(self.majorVersion).\(self.minorVersion)" - } - - public var description: String { - return "\(self.majorVersion).\(self.minorVersion).\(self.patchVersion)" - } -} diff --git a/PythonTool/main.swift b/PythonTool/main.swift deleted file mode 100644 index ddb0f1c..0000000 --- a/PythonTool/main.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// main.swift -// PythonTool -// -// Created by Pedro José Pereira Vieito on 29/1/18. -// Copyright © 2018 Pedro José Pereira Vieito. All rights reserved. -// - -import Foundation -import LoggerKit -import CommandLineKit -import PythonKit - -let listOption = BoolOption(shortFlag: "l", longFlag: "list", helpMessage: "List installed modules.") -let pathOption = BoolOption(shortFlag: "p", longFlag: "path", helpMessage: "List Python paths.") -let verboseOption = BoolOption(shortFlag: "v", longFlag: "verbose", helpMessage: "Verbose mode.") -let helpOption = BoolOption(shortFlag: "h", longFlag: "help", helpMessage: "Prints a help message.") - -let cli = CommandLineKit.CommandLine() -cli.addOptions(listOption, pathOption, verboseOption, helpOption) - -do { - try cli.parse(strict: true) -} -catch { - cli.printUsage(error) - exit(EX_USAGE) -} - -if helpOption.value { - cli.printUsage() - exit(-1) -} - -Logger.logMode = .commandLine -Logger.logLevel = verboseOption.value ? .debug : .info - -do { - let pythonVersion = OperatingSystemVersion( - majorVersion: Int(Python.versionInfo.major) ?? 0, - minorVersion: Int(Python.versionInfo.minor) ?? 0, - patchVersion: Int(Python.versionInfo.micro) ?? 0 - ) - - Logger.log(important: "Python \(pythonVersion.shortVersion)") - Logger.log(info: "Version: \(pythonVersion)") - Logger.log(verbose: "Version String: \(Python.version.splitlines()[0])") - - let sys = try Python.attemptImport("sys") - - Logger.log(verbose: "Executable: \(sys.executable)") - Logger.log(verbose: "Executable Prefix: \(sys.exec_prefix)") - - if pathOption.value { - - Logger.log(important: "Python Paths (\(sys.path.count))") - - if !sys.path.isEmpty { - for searchPath in sys.path { - Logger.log(success: searchPath) - } - } - else { - Logger.log(warning: "No paths available.") - } - } - - if listOption.value { - - let pkg_resources = try Python.attemptImport("pkg_resources") - - let installedModules = Dictionary(pkg_resources.working_set.by_key)! - - Logger.log(important: "Python Modules (\(installedModules.count))") - - if !installedModules.isEmpty { - let installedModulesArray = installedModules.sorted { $0.key.lowercased() < $1.key.lowercased() } - - for (_, moduleReference) in installedModulesArray { - Logger.log(success: "\(moduleReference.key) (\(moduleReference.version))") - } - } - else { - Logger.log(warning: "No modules available.") - } - } -} -catch { - Logger.log(fatalError: error) -} diff --git a/README.md b/README.md index e433908..8174faa 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -# PythonKit +# PythonKit -Swift framework to interact with Python. - -## Requirements +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpvieito%2FPythonKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/pvieito/PythonKit) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fpvieito%2FPythonKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/pvieito/PythonKit) -`PythonKit` requires [**Swift 5**](https://swift.org/download/) or higher and has been tested on macOS, Linux and Windows. +Swift framework to interact with Python. ## Usage @@ -18,12 +17,12 @@ print(f"Python Version: {sys.version}") print(f"Python Encoding: {sys.getdefaultencoding().upper()}") ``` -Can be implemented in Swift through PythonKit with the following code: +The same code can be implemented in Swift through PythonKit: ```swift import PythonKit -let sys = try Python.import("sys") +let sys = Python.import("sys") print("Python \(sys.version_info.major).\(sys.version_info.minor)") print("Python Version: \(sys.version)") @@ -35,37 +34,39 @@ 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", from: "1.0.0"), ``` -## Build - -`PythonKit` can be built with Swift PM: - -``` -$ cd PythonKit -$ swift run -[*] Python 3.7 -[ ] Version: 3.7.0 -``` +## Environment Variables -The Python library will be loaded at runtime, `PythonKit` will try to find the most modern Python version available in the system. You can force a given version with the `PYTHON_VERSION` environment variable or an specific Python library path or name with `PYTHON_LIBRARY`. +As Python libraries are loaded at runtime by **PythonKit**, it will try to find the most modern Python version available in the system. You can force a given version with the `PYTHON_VERSION` environment variable or a specific Python library path or name with `PYTHON_LIBRARY`. -``` +```console $ PYTHON_VERSION=3 swift run [*] Python 3.5 -[ ] Version: 3.5.2 $ PYTHON_VERSION=2.7 swift run [*] Python 2.7 -[ ] Version: 2.7.10 $ PYTHON_LIBRARY=libpython3.5.so swift run [*] Python 3.5 -[ ] Version: 3.5.2 $ PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython2.7.so swift run [*] Python 2.7 -[ ] Version: 2.7.10 ``` +If **PythonKit** cannot find and load the Python library you can set the `PYTHON_LOADER_LOGGING` environment variable to know from which locations **PythonKit** is trying to load the library: + +```console +$ PYTHON_LOADER_LOGGING=TRUE PYTHON_VERSION=3.8 swift run +Loading symbol 'Py_Initialize' from the Python library... +Trying to load library at 'Python.framework/Versions/3.8/Python'... +Trying to load library at '/usr/local/Frameworks/Python.framework/Versions/3.8/Python'... +Fatal error: Python library not found. Set the PYTHON_LIBRARY environment variable with the path to a Python library. +``` + +## Troubleshooting + +- If you are targeting the Mac platform with the [Hardened Runtime](https://developer.apple.com/documentation/security/hardened_runtime) enabled, make sure you are properly signing and embedding the Python framework you are trying to load with **PythonKit**. The Hardened Runtime [Library Validation mechanism](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_disable-library-validation) prevents a process from loading libraries that are not signed by Apple or the same developer as the main process. + ## Notes -`PythonKit` is based on the `Python` module from the [Swift for TensorFlow](https://github.com/tensorflow/swift) project. Currently both modules are equivalent, the mantainers have been developing them jointly and are kept in sync. +- Originally **PythonKit** was based on the `Python` module from the [**Swift for TensorFlow**](https://github.com/tensorflow/swift) experimental project. +- If you have questions about **PythonKit** you can ask on the [**Swift Forums**](https://forums.swift.org/c/related-projects/). diff --git a/Tests/PythonKitTests/Info.plist b/Tests/PythonKitTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/Tests/PythonKitTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Tests/PythonKitTests/NumpyConversionTests.swift b/Tests/PythonKitTests/NumpyConversionTests.swift index d798cca..4189470 100644 --- a/Tests/PythonKitTests/NumpyConversionTests.swift +++ b/Tests/PythonKitTests/NumpyConversionTests.swift @@ -2,31 +2,31 @@ import XCTest import PythonKit class NumpyConversionTests: XCTestCase { - static var numpyModule = try? Python.attemptImport("numpy") + static var numpyModule = try? Python.attemptImport("numpy") - func testArrayConversion() { - guard let np = NumpyConversionTests.numpyModule else { return } + func testArrayConversion() { + guard let np = NumpyConversionTests.numpyModule else { return } - let numpyArrayEmpty = np.array([] as [Float], dtype: np.float32) - XCTAssertEqual([], Array(numpy: numpyArrayEmpty)) + 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 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 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 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 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 - // strided array. - XCTAssertNotEqual(numpyArrayStrided.__array_interface__["strides"], Python.None) - XCTAssertEqual([2, 2], Array(numpy: numpyArrayStrided)) - } + 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 + // strided array. + XCTAssertNotEqual(numpyArrayStrided.__array_interface__["strides"], Python.None) + XCTAssertEqual([2, 2], Array(numpy: numpyArrayStrided)) + } } diff --git a/Tests/PythonKitTests/PythonFunctionTests.swift b/Tests/PythonKitTests/PythonFunctionTests.swift new file mode 100644 index 0000000..f11ad57 --- /dev/null +++ b/Tests/PythonKitTests/PythonFunctionTests.swift @@ -0,0 +1,343 @@ +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) + } +} diff --git a/Tests/PythonKitTests/PythonRuntimeTests.swift b/Tests/PythonKitTests/PythonRuntimeTests.swift index 6eb3c24..90f1418 100644 --- a/Tests/PythonKitTests/PythonRuntimeTests.swift +++ b/Tests/PythonKitTests/PythonRuntimeTests.swift @@ -2,271 +2,382 @@ import XCTest import PythonKit class PythonRuntimeTests: XCTestCase { - func testCheckVersion() { - 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]) - - polymorphicList[2] = 2 - XCTAssertEqual(2, polymorphicList[2]) - } - - func testPythonDict() { - let dict: PythonObject = ["a": 1, 1: 0.5] - XCTAssertEqual(2, Python.len(dict)) - XCTAssertEqual(1, dict["a"]) - XCTAssertEqual(0.5, dict[1]) - - dict["b"] = "c" - XCTAssertEqual("c", dict["b"]) - dict["b"] = "d" - XCTAssertEqual("d", dict["b"]) - } - - 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...))) - } - - 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) - x *= 2 - XCTAssertEqual(10, x) - x -= 3 - XCTAssertEqual(7, x) - x /= 2 - XCTAssertEqual(3.5, x) - x += -1 - XCTAssertEqual(2.5, x) - } - - func testComparable() { - let array: [PythonObject] = [-1, 10, 1, 0, 0] - XCTAssertEqual([-1, 0, 0, 1, 10], array.sorted()) - let list: PythonObject = [-1, 10, 1, 0, 0] - XCTAssertEqual([-1, 0, 0, 1, 10], list.sorted()) - } - - func testHashable() { - func compareHashValues(_ x: PythonConvertible) { - let a = x.pythonObject - let b = x.pythonObject - XCTAssertEqual(a.hashValue, b.hashValue) + func testCheckVersion() { + XCTAssertGreaterThanOrEqual(Python.versionInfo.major, 2) + XCTAssertGreaterThanOrEqual(Python.versionInfo.minor, 0) } - compareHashValues(1) - compareHashValues(3.14) - compareHashValues("asdf") - compareHashValues(PythonObject(tupleOf: 1, 2, 3)) - } + 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) - func testRangeIteration() { - for (index, val) in Python.range(5).enumerated() { - XCTAssertEqual(PythonObject(index), val) + 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) + x *= 2 + XCTAssertEqual(10, x) + x -= 3 + XCTAssertEqual(7, x) + x /= 2 + XCTAssertEqual(3.5, x) + x += -1 + XCTAssertEqual(2.5, x) + } + + func testUnaryOps() { + var x = PythonObject(5) + x = -x + XCTAssertEqual(-5, x) + x = PythonObject(-5) + x = -x + XCTAssertEqual(5, x) } - } - - func testErrors() { - XCTAssertThrowsError( - try PythonObject(1).__truediv__.throwing.dynamicallyCall(withArguments: 0) - ) { - guard let pythonError = $0 as? PythonError else { - XCTFail("non-Python error: \($0)") - return - } - XCTAssertEqual(pythonError, PythonError.exception("division by zero", traceback: nil)) + + func testComparable() { + let array: [PythonObject] = [-1, 10, 1, 0, 0] + XCTAssertEqual([-1, 0, 0, 1, 10], array.sorted()) + 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) { + let a = x.pythonObject + let b = x.pythonObject + XCTAssertEqual(a.hashValue, b.hashValue) + } + + compareHashValues(1) + compareHashValues(3.14) + compareHashValues("asdf") + compareHashValues(PythonObject(tupleOf: 1, 2, 3)) } - } - - func testTuple() { - let element1: PythonObject = 0 - let element2: PythonObject = "abc" - let element3: PythonObject = [0, 0] - let element4: PythonObject = ["a": 0, "b": "c"] - let pair = PythonObject(tupleOf: element1, element2) - 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]) - } - - 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!", - greeting.format("Hi", first: "John", last: "Smith")) - 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. - let minusOne: PythonObject = -1 - let zero: PythonObject = 0 - let five: PythonObject = 5 - let half: PythonObject = 0.5 - let string: PythonObject = "abc" - - XCTAssertEqual(-1, Int(minusOne)) - XCTAssertEqual(-1, Int8(minusOne)) - XCTAssertEqual(-1, Int16(minusOne)) - XCTAssertEqual(-1, Int32(minusOne)) - XCTAssertEqual(-1, Int64(minusOne)) - 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)) - XCTAssertEqual(5, UInt32(five)) - 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)) - XCTAssertNil(Int(string)) - 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) - XCTAssertEqual(minusOne, Int32(-1).pythonObject) - 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) - XCTAssertEqual(five, UInt32(5).pythonObject) - XCTAssertEqual(five, UInt64(5).pythonObject) - XCTAssertEqual(five, Float(5).pythonObject) - XCTAssertEqual(five, Double(5).pythonObject) - } - - // TODO: https://bugs.swift.org/browse/SR-9230 - // func testSR9230() { - // XCTAssertEqual(2, Python.len(Python.dict(a: "a", b: "b"))) - // } - - // TF-78: isType() consumed refcount for type objects like `PyBool_Type`. - func testPythonRefCount() { - let b: PythonObject = true - for _ in 0...20 { - // This triggers isType(), which used to crash after repeated invocation - // because of reduced refcount for `PyBool_Type`. - _ = Bool.init(b) + #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 case let PythonError.exception(exception, _) = $0 else { + XCTFail("non-Python error: \($0)") + return + } + XCTAssertEqual(exception.__class__.__name__, "ZeroDivisionError") + } + } + + #if !os(Windows) + func testTuple() { + let element1: PythonObject = 0 + let element2: PythonObject = "abc" + let element3: PythonObject = [0, 0] + let element4: PythonObject = ["a": 0, "b": "c"] + let pair = PythonObject(tupleOf: element1, element2) + 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!", + greeting.format("Hi", first: "John", last: "Smith")) + 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. + let minusOne: PythonObject = -1 + let zero: PythonObject = 0 + let five: PythonObject = 5 + let half: PythonObject = 0.5 + let string: PythonObject = "abc" + + #if !os(Windows) + XCTAssertEqual(-1, Int(minusOne)) + XCTAssertEqual(-1, Int8(minusOne)) + XCTAssertEqual(-1, Int16(minusOne)) + XCTAssertEqual(-1, Int32(minusOne)) + XCTAssertEqual(-1, Int64(minusOne)) + #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)) + XCTAssertEqual(5, UInt32(five)) + 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) + XCTAssertEqual(minusOne, Int32(-1).pythonObject) + 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) + XCTAssertEqual(five, UInt32(5).pythonObject) + XCTAssertEqual(five, UInt64(5).pythonObject) + 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 + for _ in 0...20 { + // This triggers isType(), which used to crash after repeated invocation + // because of reduced refcount for `PyBool_Type`. + _ = Bool.init(b) + } + } + + func testPythonBytes() { + let bytes = PythonBytes([UInt8(1), UInt8(2), UInt8(3), UInt8(4)]) + bytes.withUnsafeBytes { + XCTAssertEqual(Array($0), [1, 2, 3, 4]) + } + } + + func testPythonBytesInt8() { + let bytes = PythonBytes([Int8(1), Int8(2), Int8(3), Int8(4)]) + bytes.withUnsafeBytes { + XCTAssertEqual(Array($0), [1, 2, 3, 4]) + } + } + + func testPythonBytesNonContiguousSequence() { + let bytes = PythonBytes(CollectionOfOne(UInt8(1))) + bytes.withUnsafeBytes { + XCTAssertEqual(Array($0), [1]) + } + } + + func testPythonBytesNonContiguousSequenceInt8() { + let bytes = PythonBytes(CollectionOfOne(Int8(1))) + bytes.withUnsafeBytes { + XCTAssertEqual(Array($0), [1]) + } + } + + func testBytesConversion() { + let bytes = PythonBytes(CollectionOfOne(UInt8(1))) + let otherBytes = PythonBytes(bytes.pythonObject) + otherBytes?.withUnsafeBytes { + XCTAssertEqual(Array($0), [1]) + } + 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)!) + } } - } }