Skip to content

Commit 0f55b0f

Browse files
committed
Make sure that mngr.__exit__() is always called in a with statement, even if there is an interrupt during the call to mngr.__enter__()
1 parent 3ff2117 commit 0f55b0f

8 files changed

Lines changed: 141 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()

Modules/_testinternalcapi.c

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

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

3481+
if (PyType_Ready(&SelfInterruptingContextManager_Type) < 0) {
3482+
return 1;
3483+
}
3484+
PyModule_AddObject(module, "SelfInterruptingContextManager", (PyObject *)&SelfInterruptingContextManager_Type);
3485+
34213486
return 0;
34223487
}
34233488

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

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

0 commit comments

Comments
 (0)