From 819ea4c35517abd2d477beb9cfb4797577cf4b03 Mon Sep 17 00:00:00 2001 From: Buseong Kim Date: Sat, 6 Dec 2025 18:05:39 +0900 Subject: [PATCH 1/4] Add throwing callable wrapper --- PythonKit/Python.swift | 48 +++++++++++++++++++ Tests/PythonKitTests/PythonRuntimeTests.swift | 13 +++++ 2 files changed, 61 insertions(+) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index 35d2fcb..cacd6a5 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -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. @@ -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 = [:]) 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 diff --git a/Tests/PythonKitTests/PythonRuntimeTests.swift b/Tests/PythonKitTests/PythonRuntimeTests.swift index 90f1418..3e21464 100644 --- a/Tests/PythonKitTests/PythonRuntimeTests.swift +++ b/Tests/PythonKitTests/PythonRuntimeTests.swift @@ -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 From ee2baf2c06a7fff21a44aff82493c8f53e5ae245 Mon Sep 17 00:00:00 2001 From: Buseong Kim Date: Sun, 4 Jan 2026 19:21:32 +0900 Subject: [PATCH 2/4] fix: prevent SIGTRAP crash on Python exceptions in dynamic calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `try!` with do/catch in PythonObject.dynamicallyCall methods. Previously, any Python exception raised during dynamic calls would trigger swift_unexpectedError → SIGTRAP, crashing the app. Now returns Python.None with assertionFailure (debug-only) instead of fatal crash in release builds. Affected methods: - dynamicallyCall(withArguments:) - dynamicallyCall(withKeywordArguments:) (both overloads) --- PythonKit/Python.swift | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index cacd6a5..f3dcc66 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -666,7 +666,12 @@ public extension PythonObject { @discardableResult func dynamicallyCall( withArguments args: [PythonConvertible] = []) -> PythonObject { - return try! throwing.dynamicallyCall(withArguments: args) + do { + return try throwing.dynamicallyCall(withArguments: args) + } catch { + assertionFailure("Python error in dynamic call") + return Python.None + } } /// Call `self` with the specified arguments. @@ -676,7 +681,12 @@ public extension PythonObject { func dynamicallyCall( withKeywordArguments args: KeyValuePairs = [:]) -> PythonObject { - return try! throwing.dynamicallyCall(withKeywordArguments: args) + do { + return try throwing.dynamicallyCall(withKeywordArguments: args) + } catch { + assertionFailure("Python error in dynamic call") + return Python.None + } } /// Alias for the function above that lets the caller dynamically construct the argument list, without using a dictionary literal. @@ -685,7 +695,12 @@ public extension PythonObject { func dynamicallyCall( withKeywordArguments args: [(key: String, value: PythonConvertible)] = []) -> PythonObject { - return try! throwing.dynamicallyCall(withKeywordArguments: args) + do { + return try throwing.dynamicallyCall(withKeywordArguments: args) + } catch { + assertionFailure("Python error in dynamic call") + return Python.None + } } } From 1c42f8c305e89904c05118e8f75d6ad5564c5d01 Mon Sep 17 00:00:00 2001 From: Buseong Kim Date: Sun, 4 Jan 2026 19:32:23 +0900 Subject: [PATCH 3/4] Revert "fix: prevent SIGTRAP crash on Python exceptions in dynamic calls" This reverts commit ee2baf2c06a7fff21a44aff82493c8f53e5ae245. --- PythonKit/Python.swift | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index f3dcc66..cacd6a5 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -666,12 +666,7 @@ public extension PythonObject { @discardableResult func dynamicallyCall( withArguments args: [PythonConvertible] = []) -> PythonObject { - do { - return try throwing.dynamicallyCall(withArguments: args) - } catch { - assertionFailure("Python error in dynamic call") - return Python.None - } + return try! throwing.dynamicallyCall(withArguments: args) } /// Call `self` with the specified arguments. @@ -681,12 +676,7 @@ public extension PythonObject { func dynamicallyCall( withKeywordArguments args: KeyValuePairs = [:]) -> PythonObject { - do { - return try throwing.dynamicallyCall(withKeywordArguments: args) - } catch { - assertionFailure("Python error in dynamic call") - return Python.None - } + 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. @@ -695,12 +685,7 @@ public extension PythonObject { func dynamicallyCall( withKeywordArguments args: [(key: String, value: PythonConvertible)] = []) -> PythonObject { - do { - return try throwing.dynamicallyCall(withKeywordArguments: args) - } catch { - assertionFailure("Python error in dynamic call") - return Python.None - } + return try! throwing.dynamicallyCall(withKeywordArguments: args) } } From 11e7079b3919664fd7a076fe4213361d59b4bc24 Mon Sep 17 00:00:00 2001 From: Buseong Kim Date: Sun, 4 Jan 2026 19:34:53 +0900 Subject: [PATCH 4/4] feat: add SafeDynamicCallable for non-crashing Python calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `safeCallable` property to PythonObject that returns Python.None on Python exceptions instead of crashing. Follows the existing `throwingCallable` pattern, providing an opt-in safe path without changing default behavior. Usage: obj.safeCallable(args...) → returns Python.None on error --- PythonKit/Python.swift | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/PythonKit/Python.swift b/PythonKit/Python.swift index cacd6a5..bbbce9f 100644 --- a/PythonKit/Python.swift +++ b/PythonKit/Python.swift @@ -213,6 +213,15 @@ public extension PythonObject { var throwingCallable: ThrowingDynamicCallable { return ThrowingDynamicCallable(self) } + + /// Returns a dynamic-callable wrapper that catches Python exceptions and + /// returns `Python.None` instead of crashing. + /// + /// This keeps the existing `PythonObject` call behavior unchanged while + /// offering an opt-in path to safely handle errors without throwing. + var safeCallable: SafeDynamicCallable { + return SafeDynamicCallable(self) + } } /// An error produced by a failable Python operation. @@ -463,6 +472,49 @@ public struct ThrowingDynamicCallable { } } +/// A dynamic-callable wrapper around `PythonObject` that catches Python +/// exceptions and returns `Python.None` instead of crashing. +@dynamicCallable +public struct SafeDynamicCallable { + private var base: PythonObject + + fileprivate init(_ base: PythonObject) { + self.base = base + } + + @discardableResult + public func dynamicallyCall( + withArguments args: [PythonConvertible] = []) -> PythonObject { + do { + return try base.throwing.dynamicallyCall(withArguments: args) + } catch { + return Python.None + } + } + + @discardableResult + public func dynamicallyCall( + withKeywordArguments args: + KeyValuePairs = [:]) -> PythonObject { + do { + return try base.throwing.dynamicallyCall(withKeywordArguments: args) + } catch { + return Python.None + } + } + + @discardableResult + public func dynamicallyCall( + withKeywordArguments args: + [(key: String, value: PythonConvertible)] = []) -> PythonObject { + do { + return try base.throwing.dynamicallyCall(withKeywordArguments: args) + } catch { + return Python.None + } + } +} + //===----------------------------------------------------------------------===// // `PythonObject` member access implementation