Skip to content

Commit 2076f5f

Browse files
FIX: Fix python errors not raising in Python
- Remove PyErr_Print to avoid swallowing errors which should raise in the Python interpreter - Early return when Python is currently raising to avoid corrupting the traceback - Add error print for all failing callbacks - Improve memory management
1 parent 19122e2 commit 2076f5f

8 files changed

Lines changed: 252 additions & 56 deletions

LayerDM/MRMLDM/vtkMRMLLayerDMObjectEventObserverScripted.cxx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ vtkMRMLLayerDMObjectEventObserverScripted::vtkMRMLLayerDMObjectEventObserverScri
1111
this->SetUpdateCallback(
1212
[this](vtkObject* node, unsigned long eventId, void* callData) -> void
1313
{
14-
if (!Py_IsInitialized())
14+
if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext())
1515
{
1616
return;
1717
}
1818

1919
vtkPythonScopeGilEnsurer gilEnsurer;
20-
vtkMRMLLayerDMPythonUtil::CallPythonObject(this->m_object, vtkMRMLLayerDMPythonUtil::ToPyArgs(node, eventId, callData));
20+
if (auto result = vtkMRMLLayerDMPythonUtil::CallPythonObject(this->m_object, vtkMRMLLayerDMPythonUtil::ToPyArgs(node, eventId, callData)))
21+
{
22+
Py_DECREF(result);
23+
}
24+
else
25+
{
26+
auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->m_object) + ":";
27+
vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(this, errorMsg);
28+
}
2129
});
2230
}
2331

LayerDM/MRMLDM/vtkMRMLLayerDMPipelineScriptedCreator.cxx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ vtkMRMLLayerDMPipelineScriptedCreator::vtkMRMLLayerDMPipelineScriptedCreator()
2020
this->SetCallback(
2121
[this](vtkMRMLAbstractViewNode* viewNode, vtkMRMLNode* node) -> vtkSmartPointer<vtkMRMLLayerDMPipelineI>
2222
{
23-
if (!Py_IsInitialized())
23+
if (!vtkMRMLLayerDMPythonUtil::IsValidPythonContext())
2424
{
2525
return nullptr;
2626
}
@@ -30,6 +30,8 @@ vtkMRMLLayerDMPipelineScriptedCreator::vtkMRMLLayerDMPipelineScriptedCreator()
3030
this->m_object, vtkMRMLLayerDMPythonUtil::ToPyArgs({ vtkMRMLLayerDMPythonUtil::ToPyObject(viewNode), vtkMRMLLayerDMPythonUtil::ToPyObject(node) }));
3131
if (!result)
3232
{
33+
auto errorMsg = std::string(__func__) + ": Failed to call : " + vtkMRMLLayerDMPythonUtil::GetObjectStr(this->m_object) + ":";
34+
vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(this, errorMsg);
3335
return nullptr;
3436
}
3537
return vtkMRMLLayerDMPipelineI::SafeDownCast(vtkPythonUtil::GetPointerFromObject(result, "vtkMRMLLayerDMPipelineI"));

LayerDM/MRMLDM/vtkMRMLLayerDMPythonUtil.cxx

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "vtkMRMLLayerDMPythonUtil.h"
2+
23
#include <vtkObjectFactory.h>
34

45
vtkStandardNewMacro(vtkMRMLLayerDMPythonUtil);
@@ -62,7 +63,7 @@ vtkSmartPyObject vtkMRMLLayerDMPythonUtil::ToPyArgs(vtkObject* obj, unsigned lon
6263
PyObject* vtkMRMLLayerDMPythonUtil::CastCallData(PyObject* object, int vtkType)
6364
{
6465
vtkPythonScopeGilEnsurer gilEnsurer;
65-
if (!Py_IsInitialized())
66+
if (!IsValidPythonContext())
6667
{
6768
return nullptr;
6869
}
@@ -126,16 +127,24 @@ PyObject* vtkMRMLLayerDMPythonUtil::CastCallData(PyObject* object, int vtkType)
126127

127128
PyObject* vtkMRMLLayerDMPythonUtil::CallPythonMethod(PyObject* object, const vtkSmartPyObject& pyArgs, const std::string& fName)
128129
{
129-
if (!Py_IsInitialized() || !object)
130+
if (!IsValidPythonContext() || !object)
130131
{
131132
return nullptr;
132133
}
133134

135+
vtkPythonScopeGilEnsurer gilEnsurer;
134136
PyObject* method = PyObject_GetAttrString(object, fName.c_str());
135-
if (!method || !PyCallable_Check(method))
137+
if (!method)
138+
{
139+
// Don't modify Python's error reporting
140+
return nullptr;
141+
}
142+
143+
if (!PyCallable_Check(method))
136144
{
137-
const std::string errorString = "Method is not callable : " + fName;
138-
PyErr_SetString(PyExc_ValueError, errorString.c_str());
145+
// PyCallable_Check doesn't raise any errors. Raise called attribute isn't callable.
146+
const auto errorString = std::string("vtkMRMLLayerDMPythonUtil::") + __func__ + ": Attribute is not callable : '" + fName + "' of object : " + GetObjectStr(object);
147+
PyErr_SetString(PyExc_TypeError, errorString.c_str());
139148
return nullptr;
140149
}
141150

@@ -144,24 +153,26 @@ PyObject* vtkMRMLLayerDMPythonUtil::CallPythonMethod(PyObject* object, const vtk
144153

145154
PyObject* vtkMRMLLayerDMPythonUtil::CallPythonObject(PyObject* object, const vtkSmartPyObject& pyArgs)
146155
{
147-
if (!Py_IsInitialized() || !object || !PyCallable_Check(object))
156+
if (!IsValidPythonContext() || !object)
148157
{
149158
return nullptr;
150159
}
151160

152161
vtkPythonScopeGilEnsurer gilEnsurer;
153-
PyObject* result = PyObject_CallObject(object, pyArgs);
154-
if (!result)
162+
if (!PyCallable_Check(object))
155163
{
156-
PyErr_Print();
164+
// PyCallable_Check doesn't raise any errors. Raise called attribute isn't callable.
165+
const auto errorString = std::string("vtkMRMLLayerDMPythonUtil::") + __func__ + ": Object is not callable : " + GetObjectStr(object);
166+
PyErr_SetString(PyExc_TypeError, errorString.c_str());
157167
return nullptr;
158168
}
159-
return result;
169+
170+
return PyObject_CallObject(object, pyArgs);
160171
}
161172

162173
void vtkMRMLLayerDMPythonUtil::SetPythonObject(PyObject** destObject, PyObject* object)
163174
{
164-
if (!Py_IsInitialized())
175+
if (!IsValidPythonContext())
165176
{
166177
return;
167178
}
@@ -188,3 +199,99 @@ void vtkMRMLLayerDMPythonUtil::DeletePythonObject(PyObject** destObject)
188199
Py_XDECREF(*destObject);
189200
*destObject = nullptr;
190201
}
202+
203+
std::string vtkMRMLLayerDMPythonUtil::GetObjectStr(PyObject* object)
204+
{
205+
if (!Py_IsInitialized())
206+
{
207+
return {};
208+
}
209+
210+
if (!object)
211+
{
212+
return "None";
213+
}
214+
215+
// Save current errors to avoid changing the current python error stack if any
216+
PyObject *type, *value, *traceback;
217+
PyErr_Fetch(&type, &value, &traceback);
218+
219+
std::string objectString{ "INVALID_OBJECT_STR" };
220+
if (auto strObj = PyObject_Str(object))
221+
{
222+
objectString = PyUnicode_AsUTF8(strObj);
223+
Py_DECREF(strObj);
224+
}
225+
226+
// Restore the python error stack
227+
PyErr_Restore(type, value, traceback);
228+
return objectString;
229+
}
230+
231+
bool vtkMRMLLayerDMPythonUtil::IsValidPythonContext()
232+
{
233+
if (!Py_IsInitialized())
234+
{
235+
return false;
236+
}
237+
238+
vtkPythonScopeGilEnsurer gilEnsurer;
239+
return !PyErr_Occurred();
240+
}
241+
242+
std::string vtkMRMLLayerDMPythonUtil::FormatExceptionTraceback()
243+
{
244+
// Don't use IsValidPythonContext here as it checks if no error has occurred
245+
if (!Py_IsInitialized())
246+
{
247+
return {};
248+
}
249+
250+
vtkPythonScopeGilEnsurer gilEnsurer;
251+
if (!PyErr_Occurred())
252+
{
253+
// No error has occurred
254+
return {};
255+
}
256+
257+
// Get error string from the traceback module
258+
PyObject *type, *value, *traceback;
259+
PyErr_Fetch(&type, &value, &traceback);
260+
PyErr_NormalizeException(&type, &value, &traceback);
261+
262+
PyObject* tracebackModule = PyImport_ImportModule("traceback");
263+
PyObject* formatExceptionFunc = PyObject_GetAttrString(tracebackModule, "format_exception");
264+
PyObject* args = PyTuple_Pack(3, type, value, traceback);
265+
PyObject* formattedList = PyObject_CallObject(formatExceptionFunc, args);
266+
PyObject* emptyString = PyUnicode_FromString("");
267+
PyObject* formatted = PyUnicode_Join(emptyString, formattedList);
268+
std::string exceptionTraceback = PyUnicode_AsUTF8(formatted);
269+
270+
// Cleanup
271+
PyErr_Restore(type, value, traceback);
272+
Py_XDECREF(formatted);
273+
Py_XDECREF(emptyString);
274+
Py_XDECREF(formattedList);
275+
Py_XDECREF(args);
276+
Py_XDECREF(formatExceptionFunc);
277+
Py_XDECREF(tracebackModule);
278+
return exceptionTraceback;
279+
}
280+
281+
void vtkMRMLLayerDMPythonUtil::PrintErrorTraceback(const vtkObject* object, const std::string& errorMsg)
282+
{
283+
// If the traceback is not empty, print the traceback using vtkErrorMacro
284+
const auto traceback = FormatExceptionTraceback();
285+
if (traceback.empty())
286+
{
287+
return;
288+
}
289+
290+
std::string errorString{ errorMsg };
291+
if (!errorString.empty())
292+
{
293+
errorString += "\n";
294+
}
295+
errorString += traceback;
296+
vtkErrorWithObjectMacro(object, "" << traceback.c_str());
297+
}

LayerDM/MRMLDM/vtkMRMLLayerDMPythonUtil.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <vtkSmartPyObject.h>
99

1010
// STD includes
11+
#include <string>
1112
#include <vector>
1213

1314
/// \brief Utility class for Python/C++ interoperability in VTK MRML Layer Displayable Manager
@@ -82,6 +83,30 @@ class VTK_SLICER_LAYERDM_MODULE_MRMLDISPLAYABLEMANAGER_EXPORT vtkMRMLLayerDMPyth
8283
/// \param destObject Pointer to PyObject pointer to delete and set to nullptr
8384
static void DeletePythonObject(PyObject** destObject);
8485

86+
/// \brief true is python is initialized and no error has occurred.
87+
static bool IsValidPythonContext();
88+
89+
/// \brief returns the formatted exception string if any exception occurred.
90+
/// Can be used to warn on current errors without swallowing the exception.
91+
static std::string FormatExceptionTraceback();
92+
93+
/// \brief Outputs a vtkErrorMacro containing the traceback in case it's not empty.
94+
///
95+
/// Does nothing if the python environment is not initialized.
96+
/// Should be used by Methods that cannot properly forward Python results back to the interpreter to avoid
97+
/// silent errors happening in the Python layer (or errors being raised in unrelated parts of the executable).
98+
///
99+
/// This method doesn't swallow the underlying python exception.
100+
/// Python level exceptions should be handled at the python level where they are raised to avoid silent errors from happening.
101+
static void PrintErrorTraceback(const vtkObject* object, const std::string& errorMsg);
102+
103+
/// \brief returns the input's PyObject's string representation as std::string.
104+
/// If the python environment is not initialized, returns empty string.
105+
/// If the input object is nullptr, returns "None"
106+
/// If the input object string representation fails, return "INVALID_OBJECT_STR"
107+
/// Calling this method doesn't change the current python error stack.
108+
static std::string GetObjectStr(PyObject* object);
109+
85110
protected:
86111
vtkMRMLLayerDMPythonUtil();
87112
~vtkMRMLLayerDMPythonUtil() override;

0 commit comments

Comments
 (0)