Skip to content

Commit d24b765

Browse files
authored
Merge branch 'main' into gh-161166-stack
2 parents f1a0f95 + 313dfd8 commit d24b765

10 files changed

Lines changed: 243 additions & 89 deletions

File tree

‎.github/workflows/reusable-macos.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
--enable-slower-safety \
5252
--enable-safety \
5353
${{ inputs.free-threading && '--disable-gil' || '' }} \
54+
--with-tail-call-interp \
5455
--prefix=/opt/python-dev \
5556
--with-openssl="$(brew --prefix openssl@3.5)"
5657
- name: Build CPython

‎.github/workflows/tail-call.yml‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,6 @@ env:
2323
LLVM_VERSION: 21
2424

2525
jobs:
26-
macos:
27-
name: aarch64-apple-darwin/clang
28-
runs-on: macos-26
29-
timeout-minutes: 60
30-
steps:
31-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32-
with:
33-
persist-credentials: false
34-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
35-
with:
36-
python-version: '3.11'
37-
- name: Install dependencies
38-
run: |
39-
brew update
40-
brew install llvm@${{ env.LLVM_VERSION }}
41-
- name: Build
42-
run: |
43-
export SDKROOT="$(xcrun --show-sdk-path)"
44-
export PATH="/opt/homebrew/opt/llvm@${{ env.LLVM_VERSION }}/bin:$PATH"
45-
CC=clang-${{ env.LLVM_VERSION }} ./configure --with-tail-call-interp
46-
make all --jobs 4
47-
- name: Test
48-
run: |
49-
./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3
50-
5126
linux:
5227
name: ${{ matrix.target }}
5328
runs-on: ${{ matrix.runner }}

‎Lib/asyncio/base_tasks.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ def _task_get_stack(task, limit):
4242
elif hasattr(task._coro, 'ag_frame'):
4343
# case 3: async generators
4444
f = task._coro.ag_frame
45+
elif not task.done() and hasattr(task._coro, 'aga_gen'):
46+
# case 4: async generator's asend/athrow/aclose
47+
f = task._coro.aga_gen.ag_frame
4548
else:
46-
# case 4: unknown objects
49+
# case 5: unknown objects
4750
f = None
4851
if f is not None:
4952
while f is not None:

‎Lib/asyncio/tools.py‎

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,22 @@ def _find_cycles(graph):
128128
def dfs(v):
129129
color[v] = GREY
130130
path.append(v)
131-
for w in graph.get(v, ()):
132-
if color[w] == WHITE:
133-
dfs(w)
134-
elif color[w] == GREY: # back-edge → cycle!
135-
i = path.index(w)
136-
cycles.append(path[i:] + [w]) # make a copy
137-
color[v] = BLACK
138-
path.pop()
131+
stack = [(v, iter(graph.get(v, ())))]
132+
while stack:
133+
node, children = stack[-1]
134+
for w in children:
135+
if color[w] == WHITE:
136+
color[w] = GREY
137+
path.append(w)
138+
stack.append((w, iter(graph.get(w, ()))))
139+
break
140+
elif color[w] == GREY: # back-edge → cycle!
141+
i = path.index(w)
142+
cycles.append(path[i:] + [w]) # make a copy
143+
else:
144+
color[node] = BLACK
145+
path.pop()
146+
stack.pop()
139147

140148
for v in list(graph):
141149
if color[v] == WHITE:
@@ -167,14 +175,16 @@ def pretty(node):
167175
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
168176
return f"{flag} {labels[node]}"
169177

170-
def render(node, prefix="", last=True, buf=None):
171-
if buf is None:
172-
buf = []
173-
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
174-
new_pref = prefix + (" " if last else "│ ")
175-
kids = children.get(node, [])
176-
for i, kid in enumerate(kids):
177-
render(kid, new_pref, i == len(kids) - 1, buf)
178+
def render(root):
179+
buf = []
180+
stack = [(root, "", True)]
181+
while stack:
182+
node, prefix, last = stack.pop()
183+
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
184+
new_pref = prefix + (" " if last else "│ ")
185+
kids = children.get(node, [])
186+
for i in range(len(kids) - 1, -1, -1):
187+
stack.append((kids[i], new_pref, i == len(kids) - 1))
178188
return buf
179189

180190
return [render(root) for root in _roots(labels, children)]

‎Lib/test/test_asyncio/test_tasks.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,40 @@ async def coro():
349349
file = io.StringIO()
350350
task.print_stack(file=file)
351351
self.assertIn(' 1 / 0\n', file.getvalue())
352+
353+
def test_get_stack_async_generator(self):
354+
# gh-161178
355+
async def gen():
356+
try:
357+
yield
358+
except ValueError:
359+
pass
360+
finally:
361+
await asyncio.sleep(0)
362+
yield
363+
364+
async def runner(method, args):
365+
agen = gen()
366+
await anext(agen)
367+
task = self.new_task(self.loop, getattr(agen, method)(*args))
368+
await asyncio.sleep(0)
369+
frame = agen.ag_frame
370+
stack = task.get_stack()
371+
await task
372+
try:
373+
self.assertEqual(stack, [frame])
374+
self.assertEqual(task.get_stack(), [])
375+
finally:
376+
await agen.aclose()
377+
378+
for method, args in (
379+
('__anext__', ()),
380+
('asend', (None,)),
381+
('athrow', (ValueError(),)),
382+
('aclose', ()),
383+
):
384+
with self.subTest(method=method):
385+
self.loop.run_until_complete(runner(method, args))
352386

353387
def test_task_repr(self):
354388
self.loop.set_debug(False)

‎Lib/test/test_asyncio/test_tools.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import unittest
23

34
from asyncio import tools
@@ -1465,6 +1466,77 @@ def test_deep_coroutine_chain(self):
14651466
result = tools.build_async_tree(input_)
14661467
self.assertEqual(result, expected)
14671468

1469+
def test_deep_task_chain(self):
1470+
# gh-159168
1471+
n = sys.getrecursionlimit()
1472+
input_ = [
1473+
AwaitedInfo(
1474+
thread_id=1,
1475+
awaited_by=[
1476+
TaskInfo(
1477+
task_id=i,
1478+
task_name=f"Task-{i}",
1479+
coroutine_stack=[],
1480+
awaited_by=[
1481+
CoroInfo(
1482+
call_stack=[FrameInfo("f", "", LocationInfo(0))],
1483+
task_name=i - 1
1484+
)
1485+
] if i > 1 else []
1486+
)
1487+
for i in range(1, n + 1)
1488+
]
1489+
)
1490+
]
1491+
tree = tools.build_async_tree(input_)
1492+
self.assertEqual(len(tree[0]), 2 * n - 1)
1493+
self.assertEqual(tree[0][-1], " " * 8 * (n - 1) + f"└── (T) Task-{n}")
1494+
1495+
def test_cycle_below_task(self):
1496+
# gh-159168
1497+
input_ = [
1498+
AwaitedInfo(
1499+
thread_id=1,
1500+
awaited_by=[
1501+
TaskInfo(
1502+
task_id=1,
1503+
task_name="Task-1",
1504+
coroutine_stack=[],
1505+
awaited_by=[]
1506+
),
1507+
TaskInfo(
1508+
task_id=2,
1509+
task_name="Task-2",
1510+
coroutine_stack=[],
1511+
awaited_by=[
1512+
CoroInfo(
1513+
call_stack=[FrameInfo("f", "", LocationInfo(0))],
1514+
task_name=1
1515+
),
1516+
CoroInfo(
1517+
call_stack=[FrameInfo("f", "", LocationInfo(0))],
1518+
task_name=3
1519+
)
1520+
]
1521+
),
1522+
TaskInfo(
1523+
task_id=3,
1524+
task_name="Task-3",
1525+
coroutine_stack=[],
1526+
awaited_by=[
1527+
CoroInfo(
1528+
call_stack=[FrameInfo("f", "", LocationInfo(0))],
1529+
task_name=2
1530+
)
1531+
]
1532+
)
1533+
]
1534+
)
1535+
]
1536+
with self.assertRaises(tools.CycleFoundException) as ctx:
1537+
tools.build_async_tree(input_)
1538+
self.assertEqual(ctx.exception.cycles, [[2, 3, 2]])
1539+
14681540
def test_multiple_cycles_same_node(self):
14691541
input_ = [
14701542
AwaitedInfo(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :exc:`RecursionError` in ``python -m asyncio pstree`` on a deep chain of
2+
awaiting tasks. Patch by Timofei Ivankov.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :meth:`asyncio.Task.print_stack` to include frames from async generator
2+
operations. Patch by Timofei Ivankov

‎Modules/_remote_debugging/clinic/module.c.h‎

Lines changed: 51 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)