diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index f804823cc530dd..25605533aacf8f 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -258,6 +258,7 @@ PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subjec PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys); PyAPI_FUNC(int) _PyEval_UnpackIterable(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, PyObject **sp); PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr); +PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate); PyAPI_FUNC(void) _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame); diff --git a/Lib/test/test_cprofile.py b/Lib/test/test_cprofile.py index b46edf66bf09f8..acead07d507e4b 100644 --- a/Lib/test/test_cprofile.py +++ b/Lib/test/test_cprofile.py @@ -136,8 +136,8 @@ def test_throw(self): for func, (cc, nc, _, _, _) in pr.stats.items(): if func[2] == "": - self.assertEqual(cc, 1) - self.assertEqual(nc, 1) + self.assertEqual(cc, 2) + self.assertEqual(nc, 2) def test_bad_descriptor(self): # gh-132250 diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 094d25b88c6e61..64435a382bb578 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -1017,6 +1017,25 @@ def f(): self.assertEqual(events, expected) + # gh-140373 + def test_gen_unwind(self): + def gen(): + yield 1 + + def f(): + g = gen() + next(g) + g.close() + + recorders = ( + UnwindRecorder, + ) + events = self.get_events(f, TEST_TOOL, recorders) + expected = [ + ("unwind", GeneratorExit, "gen"), + ] + self.assertEqual(events, expected) + class LineRecorder: event_type = E.LINE diff --git a/Lib/test/test_sys_setprofile.py b/Lib/test/test_sys_setprofile.py index b2e8e8a15b67ea..e7ba97891a3635 100644 --- a/Lib/test/test_sys_setprofile.py +++ b/Lib/test/test_sys_setprofile.py @@ -272,6 +272,8 @@ def g(p): self.check_events(g, [(1, 'call', g_ident, None), (2, 'call', f_ident, None), (2, 'return', f_ident, 0), + (2, 'call', f_ident, None), + (2, 'return', f_ident, None), (1, 'return', g_ident, None), ], check_args=True) diff --git a/Misc/NEWS.d/next/Core and Builtins/2025-10-29-20-59-10.gh-issue-140373.-uoaPP.rst b/Misc/NEWS.d/next/Core and Builtins/2025-10-29-20-59-10.gh-issue-140373.-uoaPP.rst new file mode 100644 index 00000000000000..c9a97037920fda --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2025-10-29-20-59-10.gh-issue-140373.-uoaPP.rst @@ -0,0 +1,2 @@ +Correctly emit ``PY_UNWIND`` event when generator object is closed. Patch by +Mikhail Efimov. diff --git a/Objects/genobject.c b/Objects/genobject.c index 0273dea958efc1..d0aef4b01c9fb1 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -381,11 +381,12 @@ gen_close(PyGenObject *gen, PyObject *args) } _PyInterpreterFrame *frame = (_PyInterpreterFrame *)gen->gi_iframe; if (is_resume(frame->instr_ptr)) { + bool no_unwind_tools = _PyEval_NoToolsForUnwind(_PyThreadState_GET()); /* We can safely ignore the outermost try block * as it is automatically generated to handle * StopIteration. */ int oparg = frame->instr_ptr->op.arg; - if (oparg & RESUME_OPARG_DEPTH1_MASK) { + if (oparg & RESUME_OPARG_DEPTH1_MASK && no_unwind_tools) { // RESUME after YIELD_VALUE and exception depth is 1 assert((oparg & RESUME_OPARG_LOCATION_MASK) != RESUME_AT_FUNC_START); gen->gi_frame_state = FRAME_COMPLETED; diff --git a/Python/ceval.c b/Python/ceval.c index a73b362e58a47b..4b9698d7f6f295 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2271,6 +2271,10 @@ monitor_unwind(PyThreadState *tstate, do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_PY_UNWIND); } +bool +_PyEval_NoToolsForUnwind(PyThreadState *tstate) { + return no_tools_for_global_event(tstate, PY_MONITORING_EVENT_PY_UNWIND); +} static int monitor_handled(PyThreadState *tstate,