Skip to content

Commit 0138ff1

Browse files
committed
Add sys._set_exception() to expose the PyErr_SetHandledExecption function
Exposing this function allows Python code to clear/set the current exception context as returned by `sys.exception()`. It is prefixed by an underscore since this functionality is not intended to be accessed by user-written code. In order to allow `sys._set_exception(None)` to clear the current exception `_PyErr_GetTopmostException` was changed to consider `Py_None` a valid exception instead of an indication to keep traversing the stack like `NULL` is. Additionally, `PyErr_SetHandledException` was updated to add `Py_None` to the exception stack instead of `NULL`. Put together, these changes allow `PyErr_SetHandledException(NULL)` to mask the entire exception chain and "clear" the current exception state as documented in `Doc/c-api/exceptions.rst`. Furthermore, since `sys._set_exception()` uses `PyErr_SetHandledException`, this allows `sys._set_exception(None)` to clear the current exception context instead of just exposing the next exception in the stack.
1 parent e79f640 commit 0138ff1

5 files changed

Lines changed: 211 additions & 3 deletions

File tree

‎Lib/test/test_sys.py‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,123 @@ def f():
161161
self.assertIsInstance(e, ValueError)
162162
self.assertIs(exc, e)
163163

164+
class SetExceptionTests(unittest.TestCase):
165+
166+
def tearDown(self):
167+
# make sure we don't leave the global exception set
168+
sys._set_exception(None);
169+
170+
def test_set_exc_invalid_values(self):
171+
for x in (0, "1", b"2"):
172+
with self.assertRaises(TypeError):
173+
sys._set_exception(x);
174+
175+
def test_clear_exc(self):
176+
try:
177+
raise ValueError()
178+
except ValueError:
179+
self.assertIsInstance(sys.exception(), ValueError)
180+
sys._set_exception(None)
181+
self.assertIsNone(sys.exception())
182+
183+
def test_set_exc(self):
184+
exc = ValueError()
185+
self.assertIsNone(sys.exception())
186+
sys._set_exception(exc)
187+
self.assertIs(sys.exception(), exc)
188+
189+
def test_set_exc_replaced_by_new_exception_and_restored(self):
190+
exc = ValueError()
191+
sys._set_exception(exc)
192+
self.assertIs(sys.exception(), exc)
193+
try:
194+
raise TypeError()
195+
except TypeError:
196+
self.assertIsInstance(sys.exception(), TypeError)
197+
self.assertIs(sys.exception().__context__, exc)
198+
199+
self.assertIs(sys.exception(), exc)
200+
201+
def test_set_exc_popped_on_exit_except(self):
202+
exc = ValueError()
203+
try:
204+
raise TypeError()
205+
except TypeError:
206+
self.assertIsInstance(sys.exception(), TypeError)
207+
sys._set_exception(exc)
208+
self.assertIs(sys.exception(), exc)
209+
self.assertIsNone(sys.exception())
210+
211+
def test_cleared_exc_overridden_and_restored(self):
212+
try:
213+
raise ValueError()
214+
except ValueError:
215+
try:
216+
raise TypeError()
217+
except TypeError:
218+
self.assertIsInstance(sys.exception(), TypeError)
219+
sys._set_exception(None)
220+
self.assertIsNone(sys.exception())
221+
try:
222+
raise IndexError()
223+
except IndexError:
224+
self.assertIsInstance(sys.exception(), IndexError)
225+
self.assertIsNone(sys.exception().__context__)
226+
self.assertIsNone(sys.exception())
227+
self.assertIsInstance(sys.exception(), ValueError)
228+
self.assertIsNone(sys.exception())
229+
230+
def test_clear_exc_in_generator(self):
231+
def inner():
232+
self.assertIsNone(sys.exception())
233+
yield
234+
self.assertIsInstance(sys.exception(), ValueError)
235+
sys._set_exception(None)
236+
self.assertIsNone(sys.exception())
237+
yield
238+
self.assertIsNone(sys.exception())
239+
240+
# with a single exception in exc_info stack
241+
g = inner()
242+
next(g)
243+
try:
244+
raise ValueError()
245+
except:
246+
self.assertIsInstance(sys.exception(), ValueError)
247+
next(g)
248+
self.assertIsInstance(sys.exception(), ValueError)
249+
self.assertIsNone(sys.exception())
250+
with self.assertRaises(StopIteration):
251+
next(g)
252+
self.assertIsNone(sys.exception())
253+
254+
# with multiple exceptions in exc_info stack by chaining generators
255+
def outer():
256+
g = inner()
257+
self.assertIsNone(sys.exception())
258+
yield next(g)
259+
self.assertIsInstance(sys.exception(), TypeError)
260+
try:
261+
raise ValueError()
262+
except:
263+
self.assertIsInstance(sys.exception(), ValueError)
264+
self.assertIsInstance(sys.exception().__context__, TypeError)
265+
yield next(g)
266+
self.assertIsInstance(sys.exception(), ValueError)
267+
self.assertIsInstance(sys.exception(), TypeError)
268+
269+
g = outer()
270+
next(g)
271+
try:
272+
raise TypeError()
273+
except:
274+
self.assertIsInstance(sys.exception(), TypeError)
275+
next(g)
276+
self.assertIsInstance(sys.exception(), TypeError)
277+
self.assertIsNone(sys.exception())
278+
with self.assertRaises(StopIteration):
279+
next(g)
280+
self.assertIsNone(sys.exception())
164281

165282
class ExceptHookTest(unittest.TestCase):
166283

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add sys._set_exception() function that can set/clear the current exception
2+
context. Patch by Carey Metcalfe.

‎Python/clinic/sysmodule.c.h‎

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Python/errors.c‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ _PyErr_GetTopmostException(PyThreadState *tstate)
122122
{
123123
exc_info = exc_info->previous_item;
124124
}
125-
assert(!Py_IsNone(exc_info->exc_value));
126125
return exc_info;
127126
}
128127

@@ -598,7 +597,7 @@ PyErr_GetHandledException(void)
598597
void
599598
_PyErr_SetHandledException(PyThreadState *tstate, PyObject *exc)
600599
{
601-
Py_XSETREF(tstate->exc_info->exc_value, Py_XNewRef(exc == Py_None ? NULL : exc));
600+
Py_XSETREF(tstate->exc_info->exc_value, Py_XNewRef(exc ? exc : Py_None));
602601
}
603602

604603
void

‎Python/sysmodule.c‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,32 @@ sys_exception_impl(PyObject *module)
853853
Py_RETURN_NONE;
854854
}
855855

856+
/*[clinic input]
857+
sys._set_exception
858+
exception: object
859+
860+
Set the current exception.
861+
862+
Subsequent calls to sys.exception()/sys.exc_info() will return
863+
the provided exception until another exception is raised in the
864+
current thread or the execution stack returns to a frame where
865+
another exception is being handled.
866+
[clinic start generated code]*/
867+
868+
static PyObject *
869+
sys__set_exception_impl(PyObject *module, PyObject *exception)
870+
/*[clinic end generated code: output=39e119ee6b747085 input=46da3b45313a1cfa]*/
871+
{
872+
if (!Py_IsNone(exception) && !PyExceptionInstance_Check(exception)){
873+
PyErr_SetString(
874+
PyExc_TypeError,
875+
"must be an exception/None"
876+
);
877+
return NULL;
878+
}
879+
PyErr_SetHandledException(exception);
880+
Py_RETURN_NONE;
881+
}
856882

857883
/*[clinic input]
858884
sys.exc_info
@@ -2772,6 +2798,7 @@ static PyMethodDef sys_methods[] = {
27722798
SYS__CURRENT_EXCEPTIONS_METHODDEF
27732799
SYS_DISPLAYHOOK_METHODDEF
27742800
SYS_EXCEPTION_METHODDEF
2801+
SYS__SET_EXCEPTION_METHODDEF
27752802
SYS_EXC_INFO_METHODDEF
27762803
SYS_EXCEPTHOOK_METHODDEF
27772804
SYS_EXIT_METHODDEF

0 commit comments

Comments
 (0)