Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions PythonKit/Python.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ public extension PythonObject {
var throwing: ThrowingPythonObject {
return ThrowingPythonObject(self)
}

/// Returns a dynamic-callable wrapper that surfaces Python exceptions as
/// Swift errors instead of trapping.
///
/// This keeps the existing `PythonObject` call behavior unchanged while
/// offering an opt-in path to handle errors via `try`/`catch`.
var throwingCallable: ThrowingDynamicCallable {
return ThrowingDynamicCallable(self)
}
}

/// An error produced by a failable Python operation.
Expand Down Expand Up @@ -415,6 +424,45 @@ public struct ThrowingPythonObject {
}
}

/// A dynamic-callable wrapper around `PythonObject` that throws instead of
/// trapping when a Python exception is raised.
@dynamicCallable
public struct ThrowingDynamicCallable {
private var base: PythonObject

fileprivate init(_ base: PythonObject) {
self.base = base
}

/// Call `base` with the specified positional arguments.
/// - Precondition: `base` must be a Python callable.
/// - Parameter args: Positional arguments for the Python callable.
@discardableResult
public func dynamicallyCall(
withArguments args: [PythonConvertible] = []) throws -> PythonObject {
return try base.throwing.dynamicallyCall(withArguments: args)
}

/// Call `base` with the specified arguments.
/// - Precondition: `base` must be a Python callable.
/// - Parameter args: Positional or keyword arguments for the Python callable.
@discardableResult
public func dynamicallyCall(
withKeywordArguments args:
KeyValuePairs<String, PythonConvertible> = [:]) throws -> PythonObject {
return try base.throwing.dynamicallyCall(withKeywordArguments: args)
}

/// Alias for the function above that lets the caller dynamically construct the argument list without using a dictionary literal.
/// This must be called explicitly because `@dynamicCallable` does not recognize it on `PythonObject`.
@discardableResult
public func dynamicallyCall(
withKeywordArguments args:
[(key: String, value: PythonConvertible)] = []) throws -> PythonObject {
return try base.throwing.dynamicallyCall(withKeywordArguments: args)
}
}


//===----------------------------------------------------------------------===//
// `PythonObject` member access implementation
Expand Down
13 changes: 13 additions & 0 deletions Tests/PythonKitTests/PythonRuntimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ class PythonRuntimeTests: XCTestCase {
}
}

func testThrowingCallableWrapper() throws {
let intCtor = Python.int
XCTAssertEqual(try intCtor.throwingCallable("2"), 2)

XCTAssertThrowsError(try intCtor.throwingCallable("abc")) { error in
guard case let PythonError.exception(exception, _) = error else {
XCTFail("non-Python error: \(error)")
return
}
XCTAssertEqual(exception.__class__.__name__, "ValueError")
}
}

#if !os(Windows)
func testTuple() {
let element1: PythonObject = 0
Expand Down