Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,22 @@ def _find_cycles(graph):
def dfs(v):
color[v] = GREY
path.append(v)
for w in graph.get(v, ()):
if color[w] == WHITE:
dfs(w)
elif color[w] == GREY: # back-edge → cycle!
i = path.index(w)
cycles.append(path[i:] + [w]) # make a copy
color[v] = BLACK
path.pop()
stack = [(v, iter(graph.get(v, ())))]
while stack:
node, children = stack[-1]
for w in children:
if color[w] == WHITE:
color[w] = GREY
path.append(w)
stack.append((w, iter(graph.get(w, ()))))
break
elif color[w] == GREY: # back-edge → cycle!
i = path.index(w)
cycles.append(path[i:] + [w]) # make a copy
else:
color[node] = BLACK
path.pop()
stack.pop()

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

def render(node, prefix="", last=True, buf=None):
if buf is None:
buf = []
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
new_pref = prefix + (" " if last else "│ ")
kids = children.get(node, [])
for i, kid in enumerate(kids):
render(kid, new_pref, i == len(kids) - 1, buf)
def render(root):
buf = []
stack = [(root, "", True)]
while stack:
node, prefix, last = stack.pop()
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
new_pref = prefix + (" " if last else "│ ")
kids = children.get(node, [])
for i in range(len(kids) - 1, -1, -1):
stack.append((kids[i], new_pref, i == len(kids) - 1))
return buf

return [render(root) for root in _roots(labels, children)]
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_asyncio/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
import unittest

from asyncio import tools
Expand Down Expand Up @@ -1465,6 +1466,77 @@ def test_deep_coroutine_chain(self):
result = tools.build_async_tree(input_)
self.assertEqual(result, expected)

def test_deep_task_chain(self):
# gh-159168
n = sys.getrecursionlimit()
input_ = [
AwaitedInfo(
thread_id=1,
awaited_by=[
TaskInfo(
task_id=i,
task_name=f"Task-{i}",
coroutine_stack=[],
awaited_by=[
CoroInfo(
call_stack=[FrameInfo("f", "", LocationInfo(0))],
task_name=i - 1
)
] if i > 1 else []
)
for i in range(1, n + 1)
]
)
]
tree = tools.build_async_tree(input_)
self.assertEqual(len(tree[0]), 2 * n - 1)
self.assertEqual(tree[0][-1], " " * 8 * (n - 1) + f"└── (T) Task-{n}")

def test_cycle_below_task(self):
# gh-159168
input_ = [
AwaitedInfo(
thread_id=1,
awaited_by=[
TaskInfo(
task_id=1,
task_name="Task-1",
coroutine_stack=[],
awaited_by=[]
),
TaskInfo(
task_id=2,
task_name="Task-2",
coroutine_stack=[],
awaited_by=[
CoroInfo(
call_stack=[FrameInfo("f", "", LocationInfo(0))],
task_name=1
),
CoroInfo(
call_stack=[FrameInfo("f", "", LocationInfo(0))],
task_name=3
)
]
),
TaskInfo(
task_id=3,
task_name="Task-3",
coroutine_stack=[],
awaited_by=[
CoroInfo(
call_stack=[FrameInfo("f", "", LocationInfo(0))],
task_name=2
)
]
)
]
)
]
with self.assertRaises(tools.CycleFoundException) as ctx:
tools.build_async_tree(input_)
self.assertEqual(ctx.exception.cycles, [[2, 3, 2]])

def test_multiple_cycles_same_node(self):
input_ = [
AwaitedInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :exc:`RecursionError` in ``python -m asyncio pstree`` on a deep chain of
awaiting tasks. Patch by Timofei Ivankov.
Loading