Skip to content

Commit 5929014

Browse files
committed
GH-148874: Make sure that mngr.__exit__() is always called in a with statement (GH-150911)
* Even if there is an interrupt during the call to mngr.__enter__()
1 parent 430b2d5 commit 5929014

9 files changed

Lines changed: 144 additions & 37 deletions

File tree

Lib/test/test_with.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import unittest
1111
from collections import deque
1212
from contextlib import _GeneratorContextManager, contextmanager, nullcontext
13+
from _testinternalcapi import SelfInterruptingContextManager
1314

1415

1516
def do_with(obj):
@@ -850,5 +851,21 @@ def exit_raises():
850851
expected)
851852

852853

854+
class InterruptDuringEnter(unittest.TestCase):
855+
856+
def test_exit_called_after_interrupt(self):
857+
cm = SelfInterruptingContextManager()
858+
self.assertFalse(cm.within())
859+
try:
860+
with cm:
861+
self.assertTrue(cm.within())
862+
except KeyboardInterrupt:
863+
self.assertFalse(cm.within())
864+
return
865+
except:
866+
self.fail("Wrong exception raised")
867+
self.fail("No exception raised")
868+
869+
853870
if __name__ == '__main__':
854871
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ignore interrupts immediately after calling the ``__enter__`` method of a
2+
context menager in a ``with`` statement. This ensures that the ``__exit__``
3+
method is always called in a ``with`` statement.

Modules/_testinternalcapi.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3194,6 +3194,66 @@ test_thread_state_ensure_from_view_interp_switch(PyObject *self, PyObject *unuse
31943194
Py_RETURN_NONE;
31953195
}
31963196

3197+
/* Self interrupting context manager */
3198+
3199+
typedef struct {
3200+
PyObject_HEAD
3201+
int within;
3202+
} SelfInterruptingContextManagerObject;
3203+
3204+
static PyObject *
3205+
new_self_interrupting(PyTypeObject *type, PyObject *args, PyObject *kwds)
3206+
{
3207+
SelfInterruptingContextManagerObject *self =
3208+
(SelfInterruptingContextManagerObject *)type->tp_alloc(type, 0);
3209+
if (self != NULL) {
3210+
self->within = 0;
3211+
}
3212+
return (PyObject *)self;
3213+
}
3214+
3215+
static PyObject *
3216+
self_interrupting_enter(PyObject *op, PyObject *Py_UNUSED(dummy))
3217+
{
3218+
((SelfInterruptingContextManagerObject *)op)->within = 1;
3219+
PyThreadState *tstate = PyThreadState_Get();
3220+
PyObject *ki = Py_NewRef(PyExc_KeyboardInterrupt);
3221+
PyObject *old_exc = _Py_atomic_exchange_ptr(&tstate->async_exc, ki);
3222+
_Py_set_eval_breaker_bit(tstate, _PY_ASYNC_EXCEPTION_BIT);
3223+
Py_XDECREF(old_exc);
3224+
3225+
return Py_NewRef(op);
3226+
}
3227+
3228+
static PyObject *
3229+
self_interrupting_within(PyObject *op, PyObject *Py_UNUSED(dummy))
3230+
{
3231+
return PyBool_FromLong(((SelfInterruptingContextManagerObject *)op)->within);
3232+
}
3233+
3234+
static PyObject *
3235+
self_interrupting_exit(PyObject *op, PyObject *Py_UNUSED(args)) {
3236+
((SelfInterruptingContextManagerObject *)op)->within = 0;
3237+
Py_RETURN_NONE;
3238+
}
3239+
3240+
static PyMethodDef self_interrupting_methods[] = {
3241+
{"__enter__", self_interrupting_enter, METH_NOARGS, NULL},
3242+
{"within", self_interrupting_within, METH_NOARGS, NULL},
3243+
{"__exit__", self_interrupting_exit, METH_VARARGS, NULL},
3244+
{NULL, NULL} /* sentinel */
3245+
};
3246+
3247+
static PyTypeObject SelfInterruptingContextManager_Type = {
3248+
PyVarObject_HEAD_INIT(NULL, 0)
3249+
"_testcapi.SelfInterruptingContextManager",
3250+
sizeof(SelfInterruptingContextManagerObject),
3251+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE,
3252+
.tp_new = new_self_interrupting,
3253+
.tp_methods = self_interrupting_methods,
3254+
};
3255+
3256+
31973257
static PyMethodDef module_functions[] = {
31983258
{"get_configs", get_configs, METH_NOARGS},
31993259
{"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL},
@@ -3416,6 +3476,11 @@ module_exec(PyObject *module)
34163476
}
34173477
#endif
34183478

3479+
if (PyType_Ready(&SelfInterruptingContextManager_Type) < 0) {
3480+
return 1;
3481+
}
3482+
PyModule_AddObject(module, "SelfInterruptingContextManager", (PyObject *)&SelfInterruptingContextManager_Type);
3483+
34193484
return 0;
34203485
}
34213486

Modules/_testinternalcapi/test_cases.c.h

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

Python/bytecodes.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ dummy_func(
161161
}
162162

163163
replaced op(_CHECK_PERIODIC_AT_END, (--)) {
164-
int err = check_periodics(tstate);
164+
int err = check_periodics_at_end(tstate, frame);
165165
ERROR_IF(err != 0);
166166
}
167167

Python/ceval_macros.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,22 @@ check_periodics(PyThreadState *tstate) {
527527
return 0;
528528
}
529529

530+
static inline int
531+
check_periodics_at_end(PyThreadState *tstate, _PyInterpreterFrame *frame) {
532+
_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY();
533+
QSBR_QUIESCENT_STATE(tstate);
534+
if (_Py_atomic_load_uintptr_relaxed(&tstate->eval_breaker) & _PY_EVAL_EVENTS_MASK) {
535+
// Do not handle pending interrupts if the previous instruction was LOAD_SPECIAL
536+
// This may also not handle interrupts if a cache looks like LOAD_SPECIAL,
537+
// but this is benign as we won't skip periodic checks indefinitely.
538+
if (frame->instr_ptr[-1].op.code == LOAD_SPECIAL) {
539+
return 0;
540+
}
541+
return _Py_HandlePending(tstate);
542+
}
543+
return 0;
544+
}
545+
530546
// Mark the generator as executing. Returns true if the state was changed,
531547
// false if it was already executing or finished.
532548
static inline bool

0 commit comments

Comments
 (0)