Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ PyObject * _PyEval_ImportNameWithImport(
PyObject *locals, PyObject *name, PyObject *fromlist, PyObject *level);
PyAPI_FUNC(PyObject *)_PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, Py_ssize_t nargs, PyObject *kwargs);
PyAPI_FUNC(PyObject *)_PyEval_MatchKeys(PyThreadState *tstate, PyObject *map, PyObject *keys);
PyAPI_FUNC(void) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr);
PyAPI_FUNC(int) _PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *instr);
PyAPI_FUNC(bool) _PyEval_NoToolsForUnwind(PyThreadState *tstate, _PyInterpreterFrame *frame);
PyAPI_FUNC(int) _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v, int argcnt, int argcntafter, _PyStackRef *sp);
PyAPI_FUNC(void) _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame *frame);
Expand Down
92 changes: 92 additions & 0 deletions Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,31 @@ def implicit_stop_iteration(iterator=None):
# This will record a STOP_ITERATION event for the StopIteration.
self.check_events(implicit_stop_iteration, expected, recorders=recorders)

def test_raise_callback_stop_iteration_in_for(self):
class Empty:
def __iter__(self):
return self

def __next__(self):
raise StopIteration

def run():
for _ in Empty():
pass

def on_raise(code, offset, exc):
if code is run.__code__ and isinstance(exc, StopIteration):
raise RuntimeError("monitor callback failed")

try:
sys.monitoring.register_callback(TEST_TOOL, E.RAISE, on_raise)
sys.monitoring.set_events(TEST_TOOL, E.RAISE)
with self.assertRaisesRegex(RuntimeError, "monitor callback failed"):
run()
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None)

initial = [
("raise", ZeroDivisionError),
("handled", ZeroDivisionError)
Expand Down Expand Up @@ -2652,6 +2677,73 @@ def test_func(x):

class TestTier2Optimizer(CheckEvents):

@requires_specialization
def test_raise_callback_stop_iteration_in_for_tier2(self):
_testinternalcapi = import_helper.import_module("_testinternalcapi")
_opcode = import_helper.import_module("_opcode")
threshold = _testinternalcapi.SPECIALIZATION_THRESHOLD
monitoring_enabled = False

class Iterator:
def __init__(self):
self.count = 0

def __iter__(self):
return self

def __next__(self):
if self.count < threshold:
self.count += 1
return self.count
if monitoring_enabled:
sys.monitoring.set_events(TEST_TOOL, E.RAISE)
raise StopIteration

def run():
for _ in Iterator():
pass

# Warm up the loop before enabling monitoring to create the executor.
run()
code = run.__code__
executor_offset = None
executor = None
for offset in range(0, len(code.co_code), 2):
try:
candidate = _opcode.get_executor(code, offset)
except ValueError:
continue
except RuntimeError as exc:
if "Executors are not available in this build" not in str(exc):
raise
self.skipTest("requires tier-2 executor support")
if candidate is not None:
uops = [op[0] for op in candidate]
if "_ITER_NEXT_INLINE" in uops:
executor_offset = offset
executor = candidate
break
if executor is None:
self.skipTest("requires _ITER_NEXT_INLINE executor")
Comment on lines +2726 to +2727

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain in detail what this part of the test does.


callback_offset = None

def on_raise(code, offset, exc):
nonlocal callback_offset
if code is run.__code__ and isinstance(exc, StopIteration):
callback_offset = offset
raise RuntimeError("monitor callback failed")

monitoring_enabled = True
try:
sys.monitoring.register_callback(TEST_TOOL, E.RAISE, on_raise)
with self.assertRaisesRegex(RuntimeError, "monitor callback failed"):
run()
self.assertEqual(callback_offset, executor_offset)
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.RAISE, None)

def test_monitoring_already_opimized_loop(self):
_testinternalcapi = import_helper.import_module("_testinternalcapi")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix an issue where exceptions raised by a ``sys.monitoring`` callback for
``StopIteration`` could be swallowed by a ``for`` loop.
5 changes: 4 additions & 1 deletion Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -3818,7 +3818,10 @@ dummy_func(
if (item == NULL) {
if (_PyErr_Occurred(tstate)) {
if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr);
int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr);
if (err < 0) {
ERROR_NO_POP();
}
_PyErr_Clear(tstate);
}
else {
Expand Down
11 changes: 7 additions & 4 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -2542,14 +2542,14 @@ _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v,



void
int
_PyEval_MonitorRaise(PyThreadState *tstate, _PyInterpreterFrame *frame,
_Py_CODEUNIT *instr)
{
if (no_tools_for_local_event(tstate, frame, PY_MONITORING_EVENT_RAISE)) {
return;
return 0;
}
do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE);
return do_monitor_exc(tstate, frame, instr, PY_MONITORING_EVENT_RAISE);
}

bool
Expand Down Expand Up @@ -3834,7 +3834,10 @@ _PyStackRef _PyForIter_VirtualIteratorNext(PyThreadState* tstate, _PyInterpreter
if (next == NULL) {
if (_PyErr_Occurred(tstate)) {
if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
_PyEval_MonitorRaise(tstate, frame, frame->instr_ptr);
int err = _PyEval_MonitorRaise(tstate, frame, frame->instr_ptr);
if (err < 0) {
return PyStackRef_ERROR;
}
_PyErr_Clear(tstate);
}
else {
Expand Down
6 changes: 5 additions & 1 deletion Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading