Skip to content

Commit cfd5a15

Browse files
committed
support eager_start and **kwargs in create_task, matching CPython 3.11–3.14
loop.pyx.in is a new Tempita template that emits the correct create_task signature for each Python version at build time: - 3.14+: `def create_task(self, coro, **kwargs)` forwarding all kwargs to the factory/Task, matching CPython gh-128552 - 3.13: `def create_task(self, coro, *, name=None, context=None, **kwargs)` passing extra kwargs through to the factory and Task - 3.11/3.12: `def create_task(self, coro, *, name=None, context=None)` matching CPython's behaviour for those versions - <3.11: legacy context.run path setup.py uses Cython.Tempita to render loop.pyx.in → loop.pyx before calling cythonize, using the process_tempita pattern. Fixes: #746
1 parent a308f75 commit cfd5a15

4 files changed

Lines changed: 3574 additions & 39 deletions

File tree

‎setup.py‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ def finalize_options(self):
9494

9595
for extension in self.distribution.ext_modules:
9696
for i, sfile in enumerate(extension.sources):
97-
if sfile.endswith('.pyx'):
98-
prefix, ext = os.path.splitext(sfile)
97+
if sfile.endswith('.pyx') or sfile.endswith('.pyx.in'):
98+
prefix = sfile[:-len('.pyx.in')] if sfile.endswith('.pyx.in') else sfile[:-len('.pyx')]
9999
cfile = prefix + '.c'
100100

101101
if os.path.exists(cfile) and not self.cython_always:
@@ -128,8 +128,24 @@ def finalize_options(self):
128128
CYTHON_DEPENDENCY, Cython.__version__
129129
))
130130

131+
from Cython import Tempita
131132
from Cython.Build import cythonize
132133

134+
def process_tempita(source):
135+
assert source.endswith('.pyx.in')
136+
with open(source) as f:
137+
rendered = Tempita.sub(f.read())
138+
out = source[:-len('.in')] # .pyx.in -> .pyx
139+
with open(out, 'w') as f:
140+
f.write(rendered)
141+
return out
142+
143+
for extension in self.distribution.ext_modules:
144+
extension.sources = [
145+
process_tempita(s) if s.endswith('.pyx.in') else s
146+
for s in extension.sources
147+
]
148+
133149
directives = {}
134150
if self.cython_directives:
135151
for directive in self.cython_directives.split(','):
@@ -247,7 +263,7 @@ def build_extensions(self):
247263
Extension(
248264
"uvloop.loop",
249265
sources=[
250-
"uvloop/loop.pyx",
266+
"uvloop/loop.pyx.in",
251267
],
252268
extra_compile_args=MODULES_CFLAGS
253269
),

‎tests/test_base.py‎

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,8 @@ def factory(loop, coro, **kwargs):
581581
task = MyTask(coro, loop=loop, **kwargs)
582582
# Python moved the responsibility to set the name to the Task
583583
# class constructor, so MyTask.set_name is never called by
584-
# Python's create_task. Compensate for that here.
585-
if self.is_asyncio_loop() and "name" in kwargs:
584+
# create_task when name is passed as a kwarg. Compensate here.
585+
if "name" in kwargs:
586586
task.set_name(kwargs["name"])
587587
return task
588588

@@ -604,6 +604,87 @@ def factory(loop, coro, **kwargs):
604604
self.loop.set_task_factory(None)
605605
self.assertIsNone(self.loop.get_task_factory())
606606

607+
@unittest.skipUnless(sys.version_info >= (3, 14), "requires Python 3.14+")
608+
def test_create_task_eager_start_false(self):
609+
# create_task must forward **kwargs to the task factory/constructor
610+
# so callers can pass eager_start=False (or True) per-task.
611+
self.loop._process_events = mock.Mock()
612+
613+
eager_start_values = []
614+
615+
class TrackingTask(asyncio.Task):
616+
def __init__(self, coro, *, loop, eager_start=False, **kwargs):
617+
eager_start_values.append(eager_start)
618+
super().__init__(coro, loop=loop, eager_start=eager_start, **kwargs)
619+
620+
async def coro():
621+
pass
622+
623+
# Without a factory: kwargs go straight to Task.__init__
624+
task = self.loop.create_task(coro(), eager_start=False)
625+
self.loop.run_until_complete(task)
626+
627+
# With a factory that accepts **kwargs
628+
self.loop.set_task_factory(
629+
lambda loop, coro, **kwargs: TrackingTask(coro, loop=loop, **kwargs)
630+
)
631+
task = self.loop.create_task(coro(), eager_start=False)
632+
self.loop.run_until_complete(task)
633+
self.assertEqual(eager_start_values[-1], False)
634+
635+
task = self.loop.create_task(coro(), eager_start=True)
636+
self.loop.run_until_complete(task)
637+
self.assertEqual(eager_start_values[-1], True)
638+
639+
self.loop.set_task_factory(None)
640+
641+
@unittest.skipUnless(sys.version_info >= (3, 12), "requires Python 3.12+")
642+
@unittest.skipIf(sys.version_info >= (3, 14), "3.14+ tested separately")
643+
def test_eager_task_factory_313(self):
644+
# On 3.12/3.13, eager_task_factory can be used with uvloop and
645+
# tasks are started eagerly (eager_start=True) by default.
646+
self.loop._process_events = mock.Mock()
647+
648+
eager_start_values = []
649+
650+
class TrackingTask(asyncio.Task):
651+
def __init__(self, coro, *, loop, eager_start=False, **kwargs):
652+
eager_start_values.append(eager_start)
653+
super().__init__(coro, loop=loop, eager_start=eager_start, **kwargs)
654+
655+
async def coro():
656+
pass
657+
658+
self.loop.set_task_factory(
659+
asyncio.create_eager_task_factory(TrackingTask)
660+
)
661+
task = self.loop.create_task(coro())
662+
self.loop.run_until_complete(task)
663+
self.assertEqual(eager_start_values[-1], True)
664+
665+
# create_eager_task_factory-based factory also accepts eager_start=False
666+
# via the factory signature — but that must be triggered through the
667+
# factory directly, not via create_task (which doesn't forward it on
668+
# pre-3.14). Verify the factory itself works with eager_start=False.
669+
factory = asyncio.create_eager_task_factory(TrackingTask)
670+
task = factory(self.loop, coro(), eager_start=False)
671+
self.loop.run_until_complete(task)
672+
self.assertEqual(eager_start_values[-1], False)
673+
674+
self.loop.set_task_factory(None)
675+
676+
@unittest.skipIf(sys.version_info >= (3, 14), "3.14+ supports **kwargs")
677+
def test_create_task_eager_start_raises_pre314(self):
678+
# On pre-3.14, create_task does not forward eager_start, matching
679+
# CPython's BaseEventLoop.create_task signature on those versions.
680+
self.loop._process_events = mock.Mock()
681+
682+
async def coro():
683+
pass
684+
685+
with self.assertRaises(TypeError):
686+
self.loop.create_task(coro(), eager_start=False)
687+
607688
def test_shutdown_asyncgens_01(self):
608689
finalized = list()
609690

‎uvloop/loop.pyx‎

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,46 +1404,20 @@ cdef class Loop:
14041404
"""Create a Future object attached to the loop."""
14051405
return self._new_future()
14061406

1407-
def create_task(self, coro, *, name=None, context=None):
1407+
def create_task(self, coro, **kwargs):
14081408
"""Schedule a coroutine object.
14091409
14101410
Return a task object.
1411-
1412-
If name is not None, task.set_name(name) will be called if the task
1413-
object has the set_name attribute, true for default Task in CPython.
1414-
1415-
An optional keyword-only context argument allows specifying a custom
1416-
contextvars.Context for the coro to run in. The current context copy is
1417-
created when no context is provided.
14181411
"""
14191412
self._check_closed()
1420-
if PY311:
1421-
if self._task_factory is None:
1422-
task = aio_Task(coro, loop=self, context=context)
1423-
else:
1424-
task = self._task_factory(self, coro, context=context)
1425-
else:
1426-
if context is None:
1427-
if self._task_factory is None:
1428-
task = aio_Task(coro, loop=self)
1429-
else:
1430-
task = self._task_factory(self, coro)
1431-
else:
1432-
if self._task_factory is None:
1433-
task = context.run(aio_Task, coro, self)
1434-
else:
1435-
task = context.run(self._task_factory, self, coro)
1436-
1437-
# copied from asyncio.tasks._set_task_name (bpo-34270)
1438-
if name is not None:
1439-
try:
1440-
set_name = task.set_name
1441-
except AttributeError:
1442-
pass
1443-
else:
1444-
set_name(name)
1413+
if self._task_factory is not None:
1414+
return self._task_factory(self, coro, **kwargs)
1415+
task = aio_Task(coro, loop=self, **kwargs)
1416+
try:
1417+
return task
1418+
finally:
1419+
del task
14451420

1446-
return task
14471421

14481422
def set_task_factory(self, factory):
14491423
"""Set a task factory that will be used by loop.create_task().

0 commit comments

Comments
 (0)