Browse Source

fix: save stack checkpoint before pushing args in operator() (#479)

* fix: capture stack checkpoint before pushing args in operator() (#469)

* test: add regression test for operator() python error propagation (#469)
Jason Matthew Suhari 4 days ago
parent
commit
0676b21da2
2 changed files with 23 additions and 1 deletions
  1. 8 1
      include/pybind11/internal/function.h
  2. 15 0
      include/pybind11/tests/error.cpp

+ 8 - 1
include/pybind11/internal/function.h

@@ -70,6 +70,7 @@ args_proxy interface<Derived>::operator* () const {
 template <typename Derived>
 template <typename Derived>
 template <return_value_policy policy, typename... Args>
 template <return_value_policy policy, typename... Args>
 object interface<Derived>::operator() (Args&&... args) const {
 object interface<Derived>::operator() (Args&&... args) const {
+    py_StackRef p0 = py_peek(0); // checkpoint before pushing so py_clearexc can safely rewind
     py_push(ptr());
     py_push(ptr());
     py_pushnil();
     py_pushnil();
 
 
@@ -108,7 +109,13 @@ object interface<Derived>::operator() (Args&&... args) const {
 
 
     (foreach(std::forward<Args>(args)), ...);
     (foreach(std::forward<Args>(args)), ...);
 
 
-    raise_call<py_vectorcall>(argc, kwargsc);
+    if(!py_vectorcall(argc, kwargsc)) {
+        py_matchexc(tp_Exception);
+        object e = object::from_ret();
+        auto what = py_formatexc();
+        py_clearexc(p0);
+        throw python_error(what, std::move(e));
+    }
 
 
     return object::from_ret();
     return object::from_ret();
 }
 }

+ 15 - 0
include/pybind11/tests/error.cpp

@@ -79,3 +79,18 @@ TEST_F(PYBIND11_TEST, exception_cpp_to_python) {
     TEST_EXCEPTION(attribute_error, AttributeError);
     TEST_EXCEPTION(attribute_error, AttributeError);
     TEST_EXCEPTION(runtime_error, RuntimeError);
     TEST_EXCEPTION(runtime_error, RuntimeError);
 }
 }
+
+// Regression test: operator() must throw python_error instead of crashing when Python raises (#469)
+TEST_F(PYBIND11_TEST, operator_call_propagates_python_error) {
+    py::exec("def f(x):\n    raise ValueError('intentional error')");
+    py::object fn = py::eval("f");
+
+    bool caught = false;
+    try {
+        fn(py::int_(1));
+    } catch(py::python_error& e) {
+        caught = true;
+        EXPECT_TRUE(e.match(tp_ValueError));
+    }
+    EXPECT_TRUE(caught);
+}