Skip to content

Commit b09c9e0

Browse files
authored
Merge branch 'main' into fix-taskgroup-eager
2 parents b777cf4 + a734c1e commit b09c9e0

29 files changed

+444
-221
lines changed

Doc/library/calendar.rst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,33 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
3838
itself. This is the job of subclasses.
3939

4040

41-
:class:`Calendar` instances have the following methods:
41+
:class:`Calendar` instances have the following methods and attributes:
42+
43+
.. attribute:: firstweekday
44+
45+
The first weekday as an integer (0--6).
46+
47+
This property can also be set and read using
48+
:meth:`~Calendar.setfirstweekday` and
49+
:meth:`~Calendar.getfirstweekday` respectively.
50+
51+
.. method:: getfirstweekday()
52+
53+
Return an :class:`int` for the current first weekday (0--6).
54+
55+
Identical to reading the :attr:`~Calendar.firstweekday` property.
56+
57+
.. method:: setfirstweekday(firstweekday)
58+
59+
Set the first weekday to *firstweekday*, passed as an :class:`int` (0--6)
60+
61+
Identical to setting the :attr:`~Calendar.firstweekday` property.
4262

4363
.. method:: iterweekdays()
4464

4565
Return an iterator for the week day numbers that will be used for one
4666
week. The first value from the iterator will be the same as the value of
47-
the :attr:`firstweekday` property.
67+
the :attr:`~Calendar.firstweekday` property.
4868

4969

5070
.. method:: itermonthdates(year, month)

Doc/library/ctypes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,8 @@ different ways, depending on the type and number of the parameters in the call:
18121812
the COM interface as first argument, in addition to those parameters that
18131813
are specified in the :attr:`!argtypes` tuple.
18141814

1815+
.. availability:: Windows
1816+
18151817

18161818
The optional *paramflags* parameter creates foreign function wrappers with much
18171819
more functionality than the features described above.

Lib/asyncio/base_events.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,12 @@ def create_task(self, coro, *, name=None, context=None):
477477

478478
task.set_name(name)
479479

480-
return task
480+
try:
481+
return task
482+
finally:
483+
# gh-128552: prevent a refcycle of
484+
# task.exception().__traceback__->BaseEventLoop.create_task->task
485+
del task
481486

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

Lib/asyncio/taskgroups.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ def create_task(self, coro, *, name=None, context=None):
210210
# another eager task that aborts us, if so we must cancel
211211
# this task.
212212
task.cancel()
213-
return task
213+
try:
214+
return task
215+
finally:
216+
# gh-128552: prevent a refcycle of
217+
# task.exception().__traceback__->TaskGroup.create_task->task
218+
del task
214219

215220
# Since Python 3.8 Tasks propagate all exceptions correctly,
216221
# except for KeyboardInterrupt and SystemExit which are

Lib/asyncio/timeouts.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import enum
22

33
from types import TracebackType
4-
from typing import final, Optional, Type
54

65
from . import events
76
from . import exceptions
@@ -23,14 +22,13 @@ class _State(enum.Enum):
2322
EXITED = "finished"
2423

2524

26-
@final
2725
class Timeout:
2826
"""Asynchronous context manager for cancelling overdue coroutines.
2927
3028
Use `timeout()` or `timeout_at()` rather than instantiating this class directly.
3129
"""
3230

33-
def __init__(self, when: Optional[float]) -> None:
31+
def __init__(self, when: float | None) -> None:
3432
"""Schedule a timeout that will trigger at a given loop time.
3533
3634
- If `when` is `None`, the timeout will never trigger.
@@ -39,15 +37,15 @@ def __init__(self, when: Optional[float]) -> None:
3937
"""
4038
self._state = _State.CREATED
4139

42-
self._timeout_handler: Optional[events.TimerHandle] = None
43-
self._task: Optional[tasks.Task] = None
40+
self._timeout_handler: events.TimerHandle | None = None
41+
self._task: tasks.Task | None = None
4442
self._when = when
4543

46-
def when(self) -> Optional[float]:
44+
def when(self) -> float | None:
4745
"""Return the current deadline."""
4846
return self._when
4947

50-
def reschedule(self, when: Optional[float]) -> None:
48+
def reschedule(self, when: float | None) -> None:
5149
"""Reschedule the timeout."""
5250
if self._state is not _State.ENTERED:
5351
if self._state is _State.CREATED:
@@ -96,10 +94,10 @@ async def __aenter__(self) -> "Timeout":
9694

9795
async def __aexit__(
9896
self,
99-
exc_type: Optional[Type[BaseException]],
100-
exc_val: Optional[BaseException],
101-
exc_tb: Optional[TracebackType],
102-
) -> Optional[bool]:
97+
exc_type: type[BaseException] | None,
98+
exc_val: BaseException | None,
99+
exc_tb: TracebackType | None,
100+
) -> bool | None:
103101
assert self._state in (_State.ENTERED, _State.EXPIRING)
104102

105103
if self._timeout_handler is not None:
@@ -142,7 +140,7 @@ def _insert_timeout_error(exc_val: BaseException) -> None:
142140
exc_val = exc_val.__context__
143141

144142

145-
def timeout(delay: Optional[float]) -> Timeout:
143+
def timeout(delay: float | None) -> Timeout:
146144
"""Timeout async context manager.
147145
148146
Useful in cases when you want to apply timeout logic around block
@@ -162,7 +160,7 @@ def timeout(delay: Optional[float]) -> Timeout:
162160
return Timeout(loop.time() + delay if delay is not None else None)
163161

164162

165-
def timeout_at(when: Optional[float]) -> Timeout:
163+
def timeout_at(when: float | None) -> Timeout:
166164
"""Schedule the timeout at absolute time.
167165
168166
Like timeout() but argument gives absolute time in the same clock system

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Adapted with permission from the EdgeDB project;
22
# license: PSFL.
33

4+
import weakref
45
import sys
56
import gc
67
import asyncio
@@ -38,7 +39,25 @@ def no_other_refs():
3839
return [coro]
3940

4041

41-
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
42+
def set_gc_state(enabled):
43+
was_enabled = gc.isenabled()
44+
if enabled:
45+
gc.enable()
46+
else:
47+
gc.disable()
48+
return was_enabled
49+
50+
51+
@contextlib.contextmanager
52+
def disable_gc():
53+
was_enabled = set_gc_state(enabled=False)
54+
try:
55+
yield
56+
finally:
57+
set_gc_state(enabled=was_enabled)
58+
59+
60+
class BaseTestTaskGroup:
4261

4362
async def test_taskgroup_01(self):
4463

@@ -832,15 +851,15 @@ async def test_taskgroup_without_parent_task(self):
832851
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
833852
tg.create_task(coro)
834853

835-
def test_coro_closed_when_tg_closed(self):
854+
async def test_coro_closed_when_tg_closed(self):
836855
async def run_coro_after_tg_closes():
837856
async with taskgroups.TaskGroup() as tg:
838857
pass
839858
coro = asyncio.sleep(0)
840859
with self.assertRaisesRegex(RuntimeError, "is finished"):
841860
tg.create_task(coro)
842-
loop = asyncio.get_event_loop()
843-
loop.run_until_complete(run_coro_after_tg_closes())
861+
862+
await run_coro_after_tg_closes()
844863

845864
async def test_cancelling_level_preserved(self):
846865
async def raise_after(t, e):
@@ -965,6 +984,30 @@ async def coro_fn():
965984
self.assertIsInstance(exc, _Done)
966985
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
967986

987+
988+
async def test_exception_refcycles_parent_task_wr(self):
989+
"""Test that TaskGroup deletes self._parent_task and create_task() deletes task"""
990+
tg = asyncio.TaskGroup()
991+
exc = None
992+
993+
class _Done(Exception):
994+
pass
995+
996+
async def coro_fn():
997+
async with tg:
998+
raise _Done
999+
1000+
with disable_gc():
1001+
try:
1002+
async with asyncio.TaskGroup() as tg2:
1003+
task_wr = weakref.ref(tg2.create_task(coro_fn()))
1004+
except* _Done as excs:
1005+
exc = excs.exceptions[0].exceptions[0]
1006+
1007+
self.assertIsNone(task_wr())
1008+
self.assertIsInstance(exc, _Done)
1009+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
1010+
9681011
async def test_exception_refcycles_propagate_cancellation_error(self):
9691012
"""Test that TaskGroup deletes propagate_cancellation_error"""
9701013
tg = asyncio.TaskGroup()
@@ -1024,5 +1067,16 @@ async def second_task():
10241067
self.assertTrue(ran)
10251068
self.assertIsInstance(exc, MyError)
10261069

1070+
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1071+
loop_factory = asyncio.EventLoop
1072+
1073+
class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1074+
@staticmethod
1075+
def loop_factory():
1076+
loop = asyncio.EventLoop()
1077+
loop.set_task_factory(asyncio.eager_task_factory)
1078+
return loop
1079+
1080+
10271081
if __name__ == "__main__":
10281082
unittest.main()

Lib/test/test_compiler_codegen.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def test_if_expression(self):
2929
('LOAD_CONST', 0, 1),
3030
('TO_BOOL', 0, 1),
3131
('POP_JUMP_IF_FALSE', false_lbl := self.Label(), 1),
32-
('NOT_TAKEN', None, 1),
3332
('LOAD_SMALL_INT', 42, 1),
3433
('JUMP_NO_INTERRUPT', exit_lbl := self.Label()),
3534
false_lbl,

0 commit comments

Comments
 (0)