Skip to content

Commit 02cd192

Browse files
committed
Add asyncio subclass scheduling regression tests
1 parent 1bf3003 commit 02cd192

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lib/test/test_asyncio/test_futures.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,75 @@ def call_soon(self, callback, *args, context=None):
856856
self.run_briefly()
857857
self.assertEqual(bag, [42])
858858

859+
def test_set_result_uses_private_call_soon_on_subclass_loop(self):
860+
bag = []
861+
base_loop_cls = type(self.loop)
862+
863+
class CountingLoop(base_loop_cls):
864+
def __init__(self):
865+
super().__init__()
866+
self.private_calls = []
867+
868+
def _call_soon(self, callback, args, context):
869+
self.private_calls.append((callback, args, context))
870+
return super()._call_soon(callback, args, context)
871+
872+
old_loop = self.loop
873+
loop = CountingLoop()
874+
self.loop = loop
875+
self.addCleanup(setattr, self, 'loop', old_loop)
876+
self.addCleanup(loop.close)
877+
878+
f = self._new_future()
879+
cb = self._make_callback(bag, 42)
880+
f.add_done_callback(cb)
881+
f.set_result('foo')
882+
883+
self.assertEqual(len(loop.private_calls), 1)
884+
callback, args, context = loop.private_calls[0]
885+
self.assertIs(callback, cb)
886+
self.assertEqual(args, (f,))
887+
self.assertIsNotNone(context)
888+
889+
self.run_briefly()
890+
self.assertEqual(bag, [42])
891+
892+
def test_set_result_uses_instance_call_soon_shadow(self):
893+
bag = []
894+
base_loop_cls = type(self.loop)
895+
896+
class CountingLoop(base_loop_cls):
897+
pass
898+
899+
old_loop = self.loop
900+
loop = CountingLoop()
901+
self.loop = loop
902+
self.addCleanup(setattr, self, 'loop', old_loop)
903+
self.addCleanup(loop.close)
904+
905+
base_call_soon = base_loop_cls.call_soon
906+
calls = []
907+
908+
def call_soon(callback, *args, context=None):
909+
calls.append((callback, args, context))
910+
return base_call_soon(loop, callback, *args, context=context)
911+
912+
loop.call_soon = call_soon
913+
914+
f = self._new_future()
915+
cb = self._make_callback(bag, 42)
916+
f.add_done_callback(cb)
917+
f.set_result('foo')
918+
919+
self.assertEqual(len(calls), 1)
920+
callback, args, context = calls[0]
921+
self.assertIs(callback, cb)
922+
self.assertEqual(args, (f,))
923+
self.assertIsNotNone(context)
924+
925+
self.run_briefly()
926+
self.assertEqual(bag, [42])
927+
859928
def test_callbacks_remove_first_callback(self):
860929
bag = []
861930
f = self._new_future()

Lib/test/test_asyncio/test_tasks.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,66 @@ async def coro():
218218

219219
loop.run_until_complete(task)
220220

221+
def test_task_constructor_uses_private_call_soon_on_subclass_loop(self):
222+
base_loop_cls = type(self.loop)
223+
224+
class CountingLoop(base_loop_cls):
225+
def __init__(self):
226+
super().__init__()
227+
self.private_calls = []
228+
229+
def _call_soon(self, callback, args, context):
230+
self.private_calls.append((callback, args, context))
231+
return super()._call_soon(callback, args, context)
232+
233+
async def coro():
234+
return 42
235+
236+
loop = CountingLoop()
237+
self.addCleanup(loop.close)
238+
self.set_event_loop(loop)
239+
self.addCleanup(asyncio.set_event_loop, None)
240+
241+
task = self.new_task(loop, coro())
242+
self.assertEqual(len(loop.private_calls), 1)
243+
callback, args, context = loop.private_calls[0]
244+
self.assertIs(getattr(callback, '__self__', None), task)
245+
self.assertEqual(args, ())
246+
self.assertIs(task.get_context(), context)
247+
248+
loop.run_until_complete(task)
249+
250+
def test_task_constructor_uses_instance_call_soon_shadow(self):
251+
base_loop_cls = type(self.loop)
252+
253+
class CountingLoop(base_loop_cls):
254+
pass
255+
256+
async def coro():
257+
return 42
258+
259+
loop = CountingLoop()
260+
self.addCleanup(loop.close)
261+
self.set_event_loop(loop)
262+
self.addCleanup(asyncio.set_event_loop, None)
263+
264+
base_call_soon = base_loop_cls.call_soon
265+
calls = []
266+
267+
def call_soon(callback, *args, context=None):
268+
calls.append((callback, args, context))
269+
return base_call_soon(loop, callback, *args, context=context)
270+
271+
loop.call_soon = call_soon
272+
task = self.new_task(loop, coro())
273+
self.assertEqual(len(calls), 1)
274+
callback, args, context = calls[0]
275+
self.assertIs(getattr(callback, '__self__', None), task)
276+
self.assertEqual(args, ())
277+
self.assertIs(task.get_context(), context)
278+
279+
loop.run_until_complete(task)
280+
221281
def test_ensure_future_coroutine(self):
222282
async def notmuch():
223283
return 'ok'

Modules/_asynciomodule.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ get_event_loop(asyncio_state *state)
369369
static int
370370
is_asyncio_base_event_loop_subclass(PyTypeObject *loop_type)
371371
{
372+
// The _asyncio module is imported from asyncio.events while
373+
// asyncio.base_events is still initializing, so we can't cache the
374+
// BaseEventLoop type during module init without creating an import cycle.
372375
for (PyTypeObject *base = loop_type; base != NULL; base = base->tp_base) {
373376
if (strcmp(base->tp_name, "BaseEventLoop") != 0) {
374377
continue;
@@ -395,6 +398,9 @@ is_asyncio_base_event_loop_subclass(PyTypeObject *loop_type)
395398
static int
396399
loop_has_instance_call_soon_shadow(PyObject *loop)
397400
{
401+
// Respect instance-level monkeypatching such as:
402+
// loop.call_soon = custom_call_soon
403+
// These must keep using the public slow path.
398404
PyTypeObject *loop_type = Py_TYPE(loop);
399405
PyObject *attr = NULL;
400406

@@ -457,6 +463,8 @@ call_soon(asyncio_state *state, PyObject *loop, PyObject *func, PyObject *arg,
457463
is_base_event_loop_subclass &&
458464
module_name != NULL &&
459465
PyUnicode_Check(module_name) &&
466+
// Only use the private fast path when the subclass keeps the public
467+
// call_soon() contract unchanged at both the type and instance level.
460468
!has_instance_call_soon_shadow &&
461469
(is_stdlib_base_event_loop || call_soon_attr == NULL)
462470
);

0 commit comments

Comments
 (0)