Skip to content

Commit c698243

Browse files
authored
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 b52bc56 commit c698243

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
@@ -3207,6 +3207,66 @@ test_thread_state_ensure_from_view_interp_switch(PyObject *self, PyObject *unuse
32073207
Py_RETURN_NONE;
32083208
}
32093209

3210+
/* Self interrupting context manager */
3211+
3212+
typedef struct {
3213+
PyObject_HEAD
3214+
int within;
3215+
} SelfInterruptingContextManagerObject;
3216+
3217+
static PyObject *
3218+
new_self_interrupting(PyTypeObject *type, PyObject *args, PyObject *kwds)
3219+
{
3220+
SelfInterruptingContextManagerObject *self =
3221+
(SelfInterruptingContextManagerObject *)type->tp_alloc(type, 0);
3222+
if (self != NULL) {
3223+
self->within = 0;
3224+
}
3225+
return (PyObject *)self;
3226+
}
3227+
3228+
static PyObject *
3229+
self_interrupting_enter(PyObject *op, PyObject *Py_UNUSED(dummy))
3230+
{
3231+
((SelfInterruptingContextManagerObject *)op)->within = 1;
3232+
PyThreadState *tstate = PyThreadState_Get();
3233+
PyObject *ki = Py_NewRef(PyExc_KeyboardInterrupt);
3234+
PyObject *old_exc = _Py_atomic_exchange_ptr(&tstate->async_exc, ki);
3235+
_Py_set_eval_breaker_bit(tstate, _PY_ASYNC_EXCEPTION_BIT);
3236+
Py_XDECREF(old_exc);
3237+
3238+
return Py_NewRef(op);
3239+
}
3240+
3241+
static PyObject *
3242+
self_interrupting_within(PyObject *op, PyObject *Py_UNUSED(dummy))
3243+
{
3244+
return PyBool_FromLong(((SelfInterruptingContextManagerObject *)op)->within);
3245+
}
3246+
3247+
static PyObject *
3248+
self_interrupting_exit(PyObject *op, PyObject *Py_UNUSED(args)) {
3249+
((SelfInterruptingContextManagerObject *)op)->within = 0;
3250+
Py_RETURN_NONE;
3251+
}
3252+
3253+
static PyMethodDef self_interrupting_methods[] = {
3254+
{"__enter__", self_interrupting_enter, METH_NOARGS, NULL},
3255+
{"within", self_interrupting_within, METH_NOARGS, NULL},
3256+
{"__exit__", self_interrupting_exit, METH_VARARGS, NULL},
3257+
{NULL, NULL} /* sentinel */
3258+
};
3259+
3260+
static PyTypeObject SelfInterruptingContextManager_Type = {
3261+
PyVarObject_HEAD_INIT(NULL, 0)
3262+
"_testcapi.SelfInterruptingContextManager",
3263+
sizeof(SelfInterruptingContextManagerObject),
3264+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE,
3265+
.tp_new = new_self_interrupting,
3266+
.tp_methods = self_interrupting_methods,
3267+
};
3268+
3269+
32103270
static PyMethodDef module_functions[] = {
32113271
{"get_configs", get_configs, METH_NOARGS},
32123272
{"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL},
@@ -3429,6 +3489,11 @@ module_exec(PyObject *module)
34293489
}
34303490
#endif
34313491

3492+
if (PyType_Ready(&SelfInterruptingContextManager_Type) < 0) {
3493+
return 1;
3494+
}
3495+
PyModule_AddObject(module, "SelfInterruptingContextManager", (PyObject *)&SelfInterruptingContextManager_Type);
3496+
34323497
return 0;
34333498
}
34343499

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
@@ -528,6 +528,22 @@ check_periodics(PyThreadState *tstate) {
528528
return 0;
529529
}
530530

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

0 commit comments

Comments
 (0)