Skip to content

Commit 819ea4c

Browse files
committed
Add throwing callable wrapper
1 parent d030b51 commit 819ea4c

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

PythonKit/Python.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ public extension PythonObject {
204204
var throwing: ThrowingPythonObject {
205205
return ThrowingPythonObject(self)
206206
}
207+
208+
/// Returns a dynamic-callable wrapper that surfaces Python exceptions as
209+
/// Swift errors instead of trapping.
210+
///
211+
/// This keeps the existing `PythonObject` call behavior unchanged while
212+
/// offering an opt-in path to handle errors via `try`/`catch`.
213+
var throwingCallable: ThrowingDynamicCallable {
214+
return ThrowingDynamicCallable(self)
215+
}
207216
}
208217

209218
/// An error produced by a failable Python operation.
@@ -415,6 +424,45 @@ public struct ThrowingPythonObject {
415424
}
416425
}
417426

427+
/// A dynamic-callable wrapper around `PythonObject` that throws instead of
428+
/// trapping when a Python exception is raised.
429+
@dynamicCallable
430+
public struct ThrowingDynamicCallable {
431+
private var base: PythonObject
432+
433+
fileprivate init(_ base: PythonObject) {
434+
self.base = base
435+
}
436+
437+
/// Call `base` with the specified positional arguments.
438+
/// - Precondition: `base` must be a Python callable.
439+
/// - Parameter args: Positional arguments for the Python callable.
440+
@discardableResult
441+
public func dynamicallyCall(
442+
withArguments args: [PythonConvertible] = []) throws -> PythonObject {
443+
return try base.throwing.dynamicallyCall(withArguments: args)
444+
}
445+
446+
/// Call `base` with the specified arguments.
447+
/// - Precondition: `base` must be a Python callable.
448+
/// - Parameter args: Positional or keyword arguments for the Python callable.
449+
@discardableResult
450+
public func dynamicallyCall(
451+
withKeywordArguments args:
452+
KeyValuePairs<String, PythonConvertible> = [:]) throws -> PythonObject {
453+
return try base.throwing.dynamicallyCall(withKeywordArguments: args)
454+
}
455+
456+
/// Alias for the function above that lets the caller dynamically construct the argument list without using a dictionary literal.
457+
/// This must be called explicitly because `@dynamicCallable` does not recognize it on `PythonObject`.
458+
@discardableResult
459+
public func dynamicallyCall(
460+
withKeywordArguments args:
461+
[(key: String, value: PythonConvertible)] = []) throws -> PythonObject {
462+
return try base.throwing.dynamicallyCall(withKeywordArguments: args)
463+
}
464+
}
465+
418466

419467
//===----------------------------------------------------------------------===//
420468
// `PythonObject` member access implementation

Tests/PythonKitTests/PythonRuntimeTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,19 @@ class PythonRuntimeTests: XCTestCase {
198198
}
199199
}
200200

201+
func testThrowingCallableWrapper() throws {
202+
let intCtor = Python.int
203+
XCTAssertEqual(try intCtor.throwingCallable("2"), 2)
204+
205+
XCTAssertThrowsError(try intCtor.throwingCallable("abc")) { error in
206+
guard case let PythonError.exception(exception, _) = error else {
207+
XCTFail("non-Python error: \(error)")
208+
return
209+
}
210+
XCTAssertEqual(exception.__class__.__name__, "ValueError")
211+
}
212+
}
213+
201214
#if !os(Windows)
202215
func testTuple() {
203216
let element1: PythonObject = 0

0 commit comments

Comments
 (0)