GH-91048: Add utils for printing the call stack for asyncio tasks (#133284)

This commit is contained in:
Pablo Galindo Salgado 2025-05-04 02:51:57 +02:00 committed by GitHub
parent 7363e8d24d
commit 2bc8365231
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1309 additions and 90 deletions

View file

@ -543,6 +543,105 @@ configuration mechanisms).
.. seealso::
:pep:`741`.
.. _whatsnew314-asyncio-introspection:
Asyncio introspection capabilities
----------------------------------
Added a new command-line interface to inspect running Python processes using
asynchronous tasks, available via:
.. code-block:: bash
python -m asyncio ps PID
This tool inspects the given process ID (PID) and displays information about
currently running asyncio tasks. It outputs a task table: a flat
listing of all tasks, their names, their coroutine stacks, and which tasks are
awaiting them.
.. code-block:: bash
python -m asyncio pstree PID
This tool fetches the same information, but renders a visual async call tree,
showing coroutine relationships in a hierarchical format. This command is
particularly useful for debugging long-running or stuck asynchronous programs.
It can help developers quickly identify where a program is blocked, what tasks
are pending, and how coroutines are chained together.
For example given this code:
.. code-block:: python
import asyncio
async def play(track):
await asyncio.sleep(5)
print(f"🎵 Finished: {track}")
async def album(name, tracks):
async with asyncio.TaskGroup() as tg:
for track in tracks:
tg.create_task(play(track), name=track)
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(
album("Sundowning", ["TNDNBTG", "Levitate"]), name="Sundowning")
tg.create_task(
album("TMBTE", ["DYWTYLM", "Aqua Regia"]), name="TMBTE")
if __name__ == "__main__":
asyncio.run(main())
Executing the new tool on the running process will yield a table like this:
.. code-block:: bash
python -m asyncio ps 12345
tid task id task name coroutine chain awaiter name awaiter id
---------------------------------------------------------------------------------------------------------------------------------------
8138752 0x564bd3d0210 Task-1 0x0
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
or:
.. code-block:: bash
python -m asyncio pstree 12345
└── (T) Task-1
└── main
└── __aexit__
└── _aexit
├── (T) Sundowning
│ └── album
│ └── __aexit__
│ └── _aexit
│ ├── (T) TNDNBTG
│ └── (T) Levitate
└── (T) TMBTE
└── album
└── __aexit__
└── _aexit
├── (T) DYWTYLM
└── (T) Aqua Regia
If a cycle is detected in the async await graph (which could indicate a
programming issue), the tool raises an error and lists the cycle paths that
prevent tree construction.
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
Gomez Macias in :gh:`91048`.)
.. _whatsnew314-tail-call:
A new type of interpreter

View file

@ -1,5 +1,7 @@
import argparse
import ast
import asyncio
import asyncio.tools
import concurrent.futures
import contextvars
import inspect
@ -140,6 +142,36 @@ class REPLThread(threading.Thread):
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog="python3 -m asyncio",
description="Interactive asyncio shell and CLI tools",
)
subparsers = parser.add_subparsers(help="sub-commands", dest="command")
ps = subparsers.add_parser(
"ps", help="Display a table of all pending tasks in a process"
)
ps.add_argument("pid", type=int, help="Process ID to inspect")
pstree = subparsers.add_parser(
"pstree", help="Display a tree of all pending tasks in a process"
)
pstree.add_argument("pid", type=int, help="Process ID to inspect")
args = parser.parse_args()
match args.command:
case "ps":
asyncio.tools.display_awaited_by_tasks_table(args.pid)
sys.exit(0)
case "pstree":
asyncio.tools.display_awaited_by_tasks_tree(args.pid)
sys.exit(0)
case None:
pass # continue to the interactive shell
case _:
# shouldn't happen as an invalid command-line wouldn't parse
# but let's keep it for the next person adding a command
print(f"error: unhandled command {args.command}", file=sys.stderr)
parser.print_usage(file=sys.stderr)
sys.exit(1)
sys.audit("cpython.run_stdin")
if os.getenv('PYTHON_BASIC_REPL'):

212
Lib/asyncio/tools.py Normal file
View file

@ -0,0 +1,212 @@
"""Tools to analyze tasks running in asyncio programs."""
from dataclasses import dataclass
from collections import defaultdict
from itertools import count
from enum import Enum
import sys
from _remotedebugging import get_all_awaited_by
class NodeType(Enum):
COROUTINE = 1
TASK = 2
@dataclass(frozen=True)
class CycleFoundException(Exception):
"""Raised when there is a cycle when drawing the call tree."""
cycles: list[list[int]]
id2name: dict[int, str]
# ─── indexing helpers ───────────────────────────────────────────
def _index(result):
id2name, awaits = {}, []
for _thr_id, tasks in result:
for tid, tname, awaited in tasks:
id2name[tid] = tname
for stack, parent_id in awaited:
awaits.append((parent_id, stack, tid))
return id2name, awaits
def _build_tree(id2name, awaits):
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
children = defaultdict(list)
cor_names = defaultdict(dict) # (parent) -> {frame: node}
cor_id_seq = count(1)
def _cor_node(parent_key, frame_name):
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
bucket = cor_names[parent_key]
if frame_name in bucket:
return bucket[frame_name]
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
id2label[node_key] = frame_name
children[parent_key].append(node_key)
bucket[frame_name] = node_key
return node_key
# lay down parent ➜ …frames… ➜ child paths
for parent_id, stack, child_id in awaits:
cur = (NodeType.TASK, parent_id)
for frame in reversed(stack): # outer-most → inner-most
cur = _cor_node(cur, frame)
child_key = (NodeType.TASK, child_id)
if child_key not in children[cur]:
children[cur].append(child_key)
return id2label, children
def _roots(id2label, children):
all_children = {c for kids in children.values() for c in kids}
return [n for n in id2label if n not in all_children]
# ─── detect cycles in the task-to-task graph ───────────────────────
def _task_graph(awaits):
"""Return {parent_task_id: {child_task_id, …}, …}."""
g = defaultdict(set)
for parent_id, _stack, child_id in awaits:
g[parent_id].add(child_id)
return g
def _find_cycles(graph):
"""
Depth-first search for back-edges.
Returns a list of cycles (each cycle is a list of task-ids) or an
empty list if the graph is acyclic.
"""
WHITE, GREY, BLACK = 0, 1, 2
color = defaultdict(lambda: WHITE)
path, cycles = [], []
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()
for v in list(graph):
if color[v] == WHITE:
dfs(v)
return cycles
# ─── PRINT TREE FUNCTION ───────────────────────────────────────
def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
"""
Build a list of strings for pretty-print a async call tree.
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
with `task_emoji` and coroutine frames with `cor_emoji`.
"""
id2name, awaits = _index(result)
g = _task_graph(awaits)
cycles = _find_cycles(g)
if cycles:
raise CycleFoundException(cycles, id2name)
labels, children = _build_tree(id2name, awaits)
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)
return buf
return [render(root) for root in _roots(labels, children)]
def build_task_table(result):
id2name, awaits = _index(result)
table = []
for tid, tasks in result:
for task_id, task_name, awaited in tasks:
if not awaited:
table.append(
[
tid,
hex(task_id),
task_name,
"",
"",
"0x0"
]
)
for stack, awaiter_id in awaited:
coroutine_chain = " -> ".join(stack)
awaiter_name = id2name.get(awaiter_id, "Unknown")
table.append(
[
tid,
hex(task_id),
task_name,
coroutine_chain,
awaiter_name,
hex(awaiter_id),
]
)
return table
def _print_cycle_exception(exception: CycleFoundException):
print("ERROR: await-graph contains cycles cannot print a tree!", file=sys.stderr)
print("", file=sys.stderr)
for c in exception.cycles:
inames = "".join(exception.id2name.get(tid, hex(tid)) for tid in c)
print(f"cycle: {inames}", file=sys.stderr)
def _get_awaited_by_tasks(pid: int) -> list:
try:
return get_all_awaited_by(pid)
except RuntimeError as e:
while e.__context__ is not None:
e = e.__context__
print(f"Error retrieving tasks: {e}")
sys.exit(1)
def display_awaited_by_tasks_table(pid: int) -> None:
"""Build and print a table of all pending tasks under `pid`."""
tasks = _get_awaited_by_tasks(pid)
table = build_task_table(tasks)
# Print the table in a simple tabular format
print(
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
)
print("-" * 135)
for row in table:
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
def display_awaited_by_tasks_tree(pid: int) -> None:
"""Build and print a tree of all pending tasks under `pid`."""
tasks = _get_awaited_by_tasks(pid)
try:
result = build_async_tree(tasks)
except CycleFoundException as e:
_print_cycle_exception(e)
sys.exit(1)
for tree in result:
print("\n".join(tree))

View file

@ -0,0 +1,839 @@
import unittest
from asyncio import tools
# mock output of get_all_awaited_by function.
TEST_INPUTS_TREE = [
[
# test case containing a task called timer being awaited in two
# different subtasks part of a TaskGroup (root1 and root2) which call
# awaiter functions.
(
(
1,
[
(2, "Task-1", []),
(
3,
"timer",
[
[["awaiter3", "awaiter2", "awaiter"], 4],
[["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
[["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
[["awaiter3", "awaiter2", "awaiter"], 7],
],
),
(
8,
"root1",
[[["_aexit", "__aexit__", "main"], 2]],
),
(
9,
"root2",
[[["_aexit", "__aexit__", "main"], 2]],
),
(
4,
"child1_1",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
8,
]
],
),
(
6,
"child2_1",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
8,
]
],
),
(
7,
"child1_2",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
9,
]
],
),
(
5,
"child2_2",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
9,
]
],
),
],
),
(0, []),
),
(
[
[
"└── (T) Task-1",
" └── main",
" └── __aexit__",
" └── _aexit",
" ├── (T) root1",
" │ └── bloch",
" │ └── blocho_caller",
" │ └── __aexit__",
" │ └── _aexit",
" │ ├── (T) child1_1",
" │ │ └── awaiter",
" │ │ └── awaiter2",
" │ │ └── awaiter3",
" │ │ └── (T) timer",
" │ └── (T) child2_1",
" │ └── awaiter1",
" │ └── awaiter1_2",
" │ └── awaiter1_3",
" │ └── (T) timer",
" └── (T) root2",
" └── bloch",
" └── blocho_caller",
" └── __aexit__",
" └── _aexit",
" ├── (T) child1_2",
" │ └── awaiter",
" │ └── awaiter2",
" │ └── awaiter3",
" │ └── (T) timer",
" └── (T) child2_2",
" └── awaiter1",
" └── awaiter1_2",
" └── awaiter1_3",
" └── (T) timer",
]
]
),
],
[
# test case containing two roots
(
(
9,
[
(5, "Task-5", []),
(6, "Task-6", [[["main2"], 5]]),
(7, "Task-7", [[["main2"], 5]]),
(8, "Task-8", [[["main2"], 5]]),
],
),
(
10,
[
(1, "Task-1", []),
(2, "Task-2", [[["main"], 1]]),
(3, "Task-3", [[["main"], 1]]),
(4, "Task-4", [[["main"], 1]]),
],
),
(11, []),
(0, []),
),
(
[
[
"└── (T) Task-5",
" └── main2",
" ├── (T) Task-6",
" ├── (T) Task-7",
" └── (T) Task-8",
],
[
"└── (T) Task-1",
" └── main",
" ├── (T) Task-2",
" ├── (T) Task-3",
" └── (T) Task-4",
],
]
),
],
[
# test case containing two roots, one of them without subtasks
(
[
(1, [(2, "Task-5", [])]),
(
3,
[
(4, "Task-1", []),
(5, "Task-2", [[["main"], 4]]),
(6, "Task-3", [[["main"], 4]]),
(7, "Task-4", [[["main"], 4]]),
],
),
(8, []),
(0, []),
]
),
(
[
["└── (T) Task-5"],
[
"└── (T) Task-1",
" └── main",
" ├── (T) Task-2",
" ├── (T) Task-3",
" └── (T) Task-4",
],
]
),
],
]
TEST_INPUTS_CYCLES_TREE = [
[
# this test case contains a cycle: two tasks awaiting each other.
(
[
(
1,
[
(2, "Task-1", []),
(
3,
"a",
[[["awaiter2"], 4], [["main"], 2]],
),
(4, "b", [[["awaiter"], 3]]),
],
),
(0, []),
]
),
([[4, 3, 4]]),
],
[
# this test case contains two cycles
(
[
(
1,
[
(2, "Task-1", []),
(
3,
"A",
[[["nested", "nested", "task_b"], 4]],
),
(
4,
"B",
[
[["nested", "nested", "task_c"], 5],
[["nested", "nested", "task_a"], 3],
],
),
(5, "C", [[["nested", "nested"], 6]]),
(
6,
"Task-2",
[[["nested", "nested", "task_b"], 4]],
),
],
),
(0, []),
]
),
([[4, 3, 4], [4, 6, 5, 4]]),
],
]
TEST_INPUTS_TABLE = [
[
# test case containing a task called timer being awaited in two
# different subtasks part of a TaskGroup (root1 and root2) which call
# awaiter functions.
(
(
1,
[
(2, "Task-1", []),
(
3,
"timer",
[
[["awaiter3", "awaiter2", "awaiter"], 4],
[["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
[["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
[["awaiter3", "awaiter2", "awaiter"], 7],
],
),
(
8,
"root1",
[[["_aexit", "__aexit__", "main"], 2]],
),
(
9,
"root2",
[[["_aexit", "__aexit__", "main"], 2]],
),
(
4,
"child1_1",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
8,
]
],
),
(
6,
"child2_1",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
8,
]
],
),
(
7,
"child1_2",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
9,
]
],
),
(
5,
"child2_2",
[
[
["_aexit", "__aexit__", "blocho_caller", "bloch"],
9,
]
],
),
],
),
(0, []),
),
(
[
[1, "0x2", "Task-1", "", "", "0x0"],
[
1,
"0x3",
"timer",
"awaiter3 -> awaiter2 -> awaiter",
"child1_1",
"0x4",
],
[
1,
"0x3",
"timer",
"awaiter1_3 -> awaiter1_2 -> awaiter1",
"child2_2",
"0x5",
],
[
1,
"0x3",
"timer",
"awaiter1_3 -> awaiter1_2 -> awaiter1",
"child2_1",
"0x6",
],
[
1,
"0x3",
"timer",
"awaiter3 -> awaiter2 -> awaiter",
"child1_2",
"0x7",
],
[
1,
"0x8",
"root1",
"_aexit -> __aexit__ -> main",
"Task-1",
"0x2",
],
[
1,
"0x9",
"root2",
"_aexit -> __aexit__ -> main",
"Task-1",
"0x2",
],
[
1,
"0x4",
"child1_1",
"_aexit -> __aexit__ -> blocho_caller -> bloch",
"root1",
"0x8",
],
[
1,
"0x6",
"child2_1",
"_aexit -> __aexit__ -> blocho_caller -> bloch",
"root1",
"0x8",
],
[
1,
"0x7",
"child1_2",
"_aexit -> __aexit__ -> blocho_caller -> bloch",
"root2",
"0x9",
],
[
1,
"0x5",
"child2_2",
"_aexit -> __aexit__ -> blocho_caller -> bloch",
"root2",
"0x9",
],
]
),
],
[
# test case containing two roots
(
(
9,
[
(5, "Task-5", []),
(6, "Task-6", [[["main2"], 5]]),
(7, "Task-7", [[["main2"], 5]]),
(8, "Task-8", [[["main2"], 5]]),
],
),
(
10,
[
(1, "Task-1", []),
(2, "Task-2", [[["main"], 1]]),
(3, "Task-3", [[["main"], 1]]),
(4, "Task-4", [[["main"], 1]]),
],
),
(11, []),
(0, []),
),
(
[
[9, "0x5", "Task-5", "", "", "0x0"],
[9, "0x6", "Task-6", "main2", "Task-5", "0x5"],
[9, "0x7", "Task-7", "main2", "Task-5", "0x5"],
[9, "0x8", "Task-8", "main2", "Task-5", "0x5"],
[10, "0x1", "Task-1", "", "", "0x0"],
[10, "0x2", "Task-2", "main", "Task-1", "0x1"],
[10, "0x3", "Task-3", "main", "Task-1", "0x1"],
[10, "0x4", "Task-4", "main", "Task-1", "0x1"],
]
),
],
[
# test case containing two roots, one of them without subtasks
(
[
(1, [(2, "Task-5", [])]),
(
3,
[
(4, "Task-1", []),
(5, "Task-2", [[["main"], 4]]),
(6, "Task-3", [[["main"], 4]]),
(7, "Task-4", [[["main"], 4]]),
],
),
(8, []),
(0, []),
]
),
(
[
[1, "0x2", "Task-5", "", "", "0x0"],
[3, "0x4", "Task-1", "", "", "0x0"],
[3, "0x5", "Task-2", "main", "Task-1", "0x4"],
[3, "0x6", "Task-3", "main", "Task-1", "0x4"],
[3, "0x7", "Task-4", "main", "Task-1", "0x4"],
]
),
],
# CASES WITH CYCLES
[
# this test case contains a cycle: two tasks awaiting each other.
(
[
(
1,
[
(2, "Task-1", []),
(
3,
"a",
[[["awaiter2"], 4], [["main"], 2]],
),
(4, "b", [[["awaiter"], 3]]),
],
),
(0, []),
]
),
(
[
[1, "0x2", "Task-1", "", "", "0x0"],
[1, "0x3", "a", "awaiter2", "b", "0x4"],
[1, "0x3", "a", "main", "Task-1", "0x2"],
[1, "0x4", "b", "awaiter", "a", "0x3"],
]
),
],
[
# this test case contains two cycles
(
[
(
1,
[
(2, "Task-1", []),
(
3,
"A",
[[["nested", "nested", "task_b"], 4]],
),
(
4,
"B",
[
[["nested", "nested", "task_c"], 5],
[["nested", "nested", "task_a"], 3],
],
),
(5, "C", [[["nested", "nested"], 6]]),
(
6,
"Task-2",
[[["nested", "nested", "task_b"], 4]],
),
],
),
(0, []),
]
),
(
[
[1, "0x2", "Task-1", "", "", "0x0"],
[
1,
"0x3",
"A",
"nested -> nested -> task_b",
"B",
"0x4",
],
[
1,
"0x4",
"B",
"nested -> nested -> task_c",
"C",
"0x5",
],
[
1,
"0x4",
"B",
"nested -> nested -> task_a",
"A",
"0x3",
],
[
1,
"0x5",
"C",
"nested -> nested",
"Task-2",
"0x6",
],
[
1,
"0x6",
"Task-2",
"nested -> nested -> task_b",
"B",
"0x4",
],
]
),
],
]
class TestAsyncioToolsTree(unittest.TestCase):
def test_asyncio_utils(self):
for input_, tree in TEST_INPUTS_TREE:
with self.subTest(input_):
self.assertEqual(tools.build_async_tree(input_), tree)
def test_asyncio_utils_cycles(self):
for input_, cycles in TEST_INPUTS_CYCLES_TREE:
with self.subTest(input_):
try:
tools.build_async_tree(input_)
except tools.CycleFoundException as e:
self.assertEqual(e.cycles, cycles)
class TestAsyncioToolsTable(unittest.TestCase):
def test_asyncio_utils(self):
for input_, table in TEST_INPUTS_TABLE:
with self.subTest(input_):
self.assertEqual(tools.build_task_table(input_), table)
class TestAsyncioToolsBasic(unittest.TestCase):
def test_empty_input_tree(self):
"""Test build_async_tree with empty input."""
result = []
expected_output = []
self.assertEqual(tools.build_async_tree(result), expected_output)
def test_empty_input_table(self):
"""Test build_task_table with empty input."""
result = []
expected_output = []
self.assertEqual(tools.build_task_table(result), expected_output)
def test_only_independent_tasks_tree(self):
input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
expected = [["└── (T) taskA"], ["└── (T) taskB"]]
result = tools.build_async_tree(input_)
self.assertEqual(sorted(result), sorted(expected))
def test_only_independent_tasks_table(self):
input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
self.assertEqual(
tools.build_task_table(input_),
[[1, "0xa", "taskA", "", "", "0x0"], [1, "0xb", "taskB", "", "", "0x0"]],
)
def test_single_task_tree(self):
"""Test build_async_tree with a single task and no awaits."""
result = [
(
1,
[
(2, "Task-1", []),
],
)
]
expected_output = [
[
"└── (T) Task-1",
]
]
self.assertEqual(tools.build_async_tree(result), expected_output)
def test_single_task_table(self):
"""Test build_task_table with a single task and no awaits."""
result = [
(
1,
[
(2, "Task-1", []),
],
)
]
expected_output = [[1, "0x2", "Task-1", "", "", "0x0"]]
self.assertEqual(tools.build_task_table(result), expected_output)
def test_cycle_detection(self):
"""Test build_async_tree raises CycleFoundException for cyclic input."""
result = [
(
1,
[
(2, "Task-1", [[["main"], 3]]),
(3, "Task-2", [[["main"], 2]]),
],
)
]
with self.assertRaises(tools.CycleFoundException) as context:
tools.build_async_tree(result)
self.assertEqual(context.exception.cycles, [[3, 2, 3]])
def test_complex_tree(self):
"""Test build_async_tree with a more complex tree structure."""
result = [
(
1,
[
(2, "Task-1", []),
(3, "Task-2", [[["main"], 2]]),
(4, "Task-3", [[["main"], 3]]),
],
)
]
expected_output = [
[
"└── (T) Task-1",
" └── main",
" └── (T) Task-2",
" └── main",
" └── (T) Task-3",
]
]
self.assertEqual(tools.build_async_tree(result), expected_output)
def test_complex_table(self):
"""Test build_task_table with a more complex tree structure."""
result = [
(
1,
[
(2, "Task-1", []),
(3, "Task-2", [[["main"], 2]]),
(4, "Task-3", [[["main"], 3]]),
],
)
]
expected_output = [
[1, "0x2", "Task-1", "", "", "0x0"],
[1, "0x3", "Task-2", "main", "Task-1", "0x2"],
[1, "0x4", "Task-3", "main", "Task-2", "0x3"],
]
self.assertEqual(tools.build_task_table(result), expected_output)
def test_deep_coroutine_chain(self):
input_ = [
(
1,
[
(10, "leaf", [[["c1", "c2", "c3", "c4", "c5"], 11]]),
(11, "root", []),
],
)
]
expected = [
[
"└── (T) root",
" └── c5",
" └── c4",
" └── c3",
" └── c2",
" └── c1",
" └── (T) leaf",
]
]
result = tools.build_async_tree(input_)
self.assertEqual(result, expected)
def test_multiple_cycles_same_node(self):
input_ = [
(
1,
[
(1, "Task-A", [[["call1"], 2]]),
(2, "Task-B", [[["call2"], 3]]),
(3, "Task-C", [[["call3"], 1], [["call4"], 2]]),
],
)
]
with self.assertRaises(tools.CycleFoundException) as ctx:
tools.build_async_tree(input_)
cycles = ctx.exception.cycles
self.assertTrue(any(set(c) == {1, 2, 3} for c in cycles))
def test_table_output_format(self):
input_ = [(1, [(1, "Task-A", [[["foo"], 2]]), (2, "Task-B", [])])]
table = tools.build_task_table(input_)
for row in table:
self.assertEqual(len(row), 6)
self.assertIsInstance(row[0], int) # thread ID
self.assertTrue(
isinstance(row[1], str) and row[1].startswith("0x")
) # hex task ID
self.assertIsInstance(row[2], str) # task name
self.assertIsInstance(row[3], str) # coroutine chain
self.assertIsInstance(row[4], str) # awaiter name
self.assertTrue(
isinstance(row[5], str) and row[5].startswith("0x")
) # hex awaiter ID
class TestAsyncioToolsEdgeCases(unittest.TestCase):
def test_task_awaits_self(self):
"""A task directly awaits itself should raise a cycle."""
input_ = [(1, [(1, "Self-Awaiter", [[["loopback"], 1]])])]
with self.assertRaises(tools.CycleFoundException) as ctx:
tools.build_async_tree(input_)
self.assertIn([1, 1], ctx.exception.cycles)
def test_task_with_missing_awaiter_id(self):
"""Awaiter ID not in task list should not crash, just show 'Unknown'."""
input_ = [(1, [(1, "Task-A", [[["coro"], 999]])])] # 999 not defined
table = tools.build_task_table(input_)
self.assertEqual(len(table), 1)
self.assertEqual(table[0][4], "Unknown")
def test_duplicate_coroutine_frames(self):
"""Same coroutine frame repeated under a parent should deduplicate."""
input_ = [
(
1,
[
(1, "Task-1", [[["frameA"], 2], [["frameA"], 3]]),
(2, "Task-2", []),
(3, "Task-3", []),
],
)
]
tree = tools.build_async_tree(input_)
# Both children should be under the same coroutine node
flat = "\n".join(tree[0])
self.assertIn("frameA", flat)
self.assertIn("Task-2", flat)
self.assertIn("Task-1", flat)
flat = "\n".join(tree[1])
self.assertIn("frameA", flat)
self.assertIn("Task-3", flat)
self.assertIn("Task-1", flat)
def test_task_with_no_name(self):
"""Task with no name in id2name should still render with fallback."""
input_ = [(1, [(1, "root", [[["f1"], 2]]), (2, None, [])])]
# If name is None, fallback to string should not crash
tree = tools.build_async_tree(input_)
self.assertIn("(T) None", "\n".join(tree[0]))
def test_tree_rendering_with_custom_emojis(self):
"""Pass custom emojis to the tree renderer."""
input_ = [(1, [(1, "MainTask", [[["f1", "f2"], 2]]), (2, "SubTask", [])])]
tree = tools.build_async_tree(input_, task_emoji="🧵", cor_emoji="🔁")
flat = "\n".join(tree[0])
self.assertIn("🧵 MainTask", flat)
self.assertIn("🔁 f1", flat)
self.assertIn("🔁 f2", flat)
self.assertIn("🧵 SubTask", flat)

View file

@ -4,7 +4,8 @@ import textwrap
import importlib
import sys
import socket
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
from unittest.mock import ANY
from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled
from test.support.script_helper import make_script
from test.support.socket_helper import find_unused_port
@ -13,13 +14,13 @@ import subprocess
PROCESS_VM_READV_SUPPORTED = False
try:
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
from _testexternalinspection import get_stack_trace
from _testexternalinspection import get_async_stack_trace
from _testexternalinspection import get_all_awaited_by
from _remotedebugging import PROCESS_VM_READV_SUPPORTED
from _remotedebugging import get_stack_trace
from _remotedebugging import get_async_stack_trace
from _remotedebugging import get_all_awaited_by
except ImportError:
raise unittest.SkipTest(
"Test only runs when _testexternalinspection is available")
"Test only runs when _remotedebuggingmodule is available")
def _make_test_script(script_dir, script_basename, source):
to_return = make_script(script_dir, script_basename, source)
@ -184,13 +185,13 @@ class TestGetStackTrace(unittest.TestCase):
root_task = "Task-1"
expected_stack_trace = [
["c5", "c4", "c3", "c2"],
"c2_root",
['c5', 'c4', 'c3', 'c2'],
'c2_root',
[
[["main"], root_task, []],
[["c1"], "sub_main_1", [[["main"], root_task, []]]],
[["c1"], "sub_main_2", [[["main"], root_task, []]]],
],
[['_aexit', '__aexit__', 'main'], root_task, []],
[['c1'], 'sub_main_1', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
[['c1'], 'sub_main_2', [[['_aexit', '__aexit__', 'main'], root_task, []]]],
]
]
self.assertEqual(stack_trace, expected_stack_trace)
@ -397,12 +398,15 @@ class TestGetStackTrace(unittest.TestCase):
# sets are unordered, so we want to sort "awaited_by"s
stack_trace[2].sort(key=lambda x: x[1])
expected_stack_trace = [
['deep', 'c1', 'run_one_coro'], 'Task-2', [[['main'], 'Task-1', []]]
expected_stack_trace = [
['deep', 'c1', 'run_one_coro'],
'Task-2',
[[['staggered_race', 'main'], 'Task-1', []]]
]
self.assertEqual(stack_trace, expected_stack_trace)
@skip_if_not_supported
@requires_gil_enabled("gh-133359: occasionally flaky on AMD64")
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support")
def test_async_global_awaited_by(self):
@ -516,19 +520,19 @@ class TestGetStackTrace(unittest.TestCase):
# expected: at least 1000 pending tasks
self.assertGreaterEqual(len(entries), 1000)
# the first three tasks stem from the code structure
self.assertIn(('Task-1', []), entries)
self.assertIn(('server task', [[['main'], 'Task-1', []]]), entries)
self.assertIn(('echo client spam', [[['main'], 'Task-1', []]]), entries)
self.assertIn((ANY, 'Task-1', []), entries)
self.assertIn((ANY, 'server task', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
self.assertIn((ANY, 'echo client spam', [[['_aexit', '__aexit__', 'main'], ANY]]), entries)
expected_stack = [[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]]
tasks_with_stack = [task for task in entries if task[1] == expected_stack]
expected_stack = [[['_aexit', '__aexit__', 'echo_client_spam'], ANY]]
tasks_with_stack = [task for task in entries if task[2] == expected_stack]
self.assertGreaterEqual(len(tasks_with_stack), 1000)
# the final task will have some random number, but it should for
# sure be one of the echo client spam horde (In windows this is not true
# for some reason)
if sys.platform != "win32":
self.assertEqual([[['echo_client_spam'], 'echo client spam', [[['main'], 'Task-1', []]]]], entries[-1][1])
self.assertEqual([[['_aexit', '__aexit__', 'echo_client_spam'], ANY]], entries[-1][2])
except PermissionError:
self.skipTest(
"Insufficient permissions to read the stack trace")
@ -544,7 +548,6 @@ class TestGetStackTrace(unittest.TestCase):
"Test only runs on Linux with process_vm_readv support")
def test_self_trace(self):
stack_trace = get_stack_trace(os.getpid())
print(stack_trace)
self.assertEqual(stack_trace[0], "test_self_trace")
if __name__ == "__main__":

View file

@ -1960,7 +1960,7 @@ def _supports_remote_attaching():
PROCESS_VM_READV_SUPPORTED = False
try:
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
from _remotedebuggingmodule import PROCESS_VM_READV_SUPPORTED
except ImportError:
pass

View file

@ -0,0 +1,6 @@
Add a new ``python -m asyncio ps PID`` command-line interface to inspect
asyncio tasks in a running Python process. Displays a flat table of await
relationships. A variant showing a tree view is also available as
``python -m asyncio pstree PID``. Both are useful for debugging async
code. Patch by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
Gomez Macias.

View file

@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
#_testcapi _testcapimodule.c
#_testimportmultiple _testimportmultiple.c
#_testmultiphase _testmultiphase.c
#_testexternalinspection _testexternalinspection.c
#_remotedebugging _remotedebuggingmodule.c
#_testsinglephase _testsinglephase.c
# ---

View file

@ -33,6 +33,7 @@
# Modules that should always be present (POSIX and Windows):
@MODULE_ARRAY_TRUE@array arraymodule.c
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
@MODULE__REMOTEDEBUGGING_TRUE@_remotedebugging _remotedebuggingmodule.c
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
@MODULE__CSV_TRUE@_csv _csv.c
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
@ -186,7 +187,6 @@
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
@MODULE__TESTSINGLEPHASE_TRUE@_testsinglephase _testsinglephase.c
@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
# Limited API template modules; must be built as shared modules.

View file

@ -152,9 +152,9 @@ read_char(proc_handle_t *handle, uintptr_t address, char *result)
}
static int
read_int(proc_handle_t *handle, uintptr_t address, int *result)
read_sized_int(proc_handle_t *handle, uintptr_t address, void *result, size_t size)
{
int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(int), result);
int res = _Py_RemoteDebug_ReadRemoteMemory(handle, address, size, result);
if (res < 0) {
return -1;
}
@ -345,7 +345,7 @@ parse_coro_chain(
uintptr_t gen_type_addr;
int err = read_ptr(
handle,
coro_address + sizeof(void*),
coro_address + offsets->pyobject.ob_type,
&gen_type_addr);
if (err) {
return -1;
@ -376,11 +376,13 @@ parse_coro_chain(
}
Py_DECREF(name);
int gi_frame_state;
err = read_int(
int8_t gi_frame_state;
err = read_sized_int(
handle,
coro_address + offsets->gen_object.gi_frame_state,
&gi_frame_state);
&gi_frame_state,
sizeof(int8_t)
);
if (err) {
return -1;
}
@ -427,7 +429,7 @@ parse_coro_chain(
uintptr_t gi_await_addr_type_addr;
int err = read_ptr(
handle,
gi_await_addr + sizeof(void*),
gi_await_addr + offsets->pyobject.ob_type,
&gi_await_addr_type_addr);
if (err) {
return -1;
@ -470,7 +472,8 @@ parse_task_awaited_by(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
PyObject *awaited_by
PyObject *awaited_by,
int recurse_task
);
@ -480,7 +483,8 @@ parse_task(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
PyObject *render_to
PyObject *render_to,
int recurse_task
) {
char is_task;
int err = read_char(
@ -508,8 +512,13 @@ parse_task(
Py_DECREF(call_stack);
if (is_task) {
PyObject *tn = parse_task_name(
handle, offsets, async_offsets, task_address);
PyObject *tn = NULL;
if (recurse_task) {
tn = parse_task_name(
handle, offsets, async_offsets, task_address);
} else {
tn = PyLong_FromUnsignedLongLong(task_address);
}
if (tn == NULL) {
goto err;
}
@ -550,21 +559,23 @@ parse_task(
goto err;
}
PyObject *awaited_by = PyList_New(0);
if (awaited_by == NULL) {
goto err;
}
if (PyList_Append(result, awaited_by)) {
if (recurse_task) {
PyObject *awaited_by = PyList_New(0);
if (awaited_by == NULL) {
goto err;
}
if (PyList_Append(result, awaited_by)) {
Py_DECREF(awaited_by);
goto err;
}
/* we can operate on a borrowed one to simplify cleanup */
Py_DECREF(awaited_by);
goto err;
}
/* we can operate on a borrowed one to simplify cleanup */
Py_DECREF(awaited_by);
if (parse_task_awaited_by(handle, offsets, async_offsets,
task_address, awaited_by)
) {
goto err;
if (parse_task_awaited_by(handle, offsets, async_offsets,
task_address, awaited_by, 1)
) {
goto err;
}
}
Py_DECREF(result);
@ -581,7 +592,8 @@ parse_tasks_in_set(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t set_addr,
PyObject *awaited_by
PyObject *awaited_by,
int recurse_task
) {
uintptr_t set_obj;
if (read_py_ptr(
@ -642,7 +654,9 @@ parse_tasks_in_set(
offsets,
async_offsets,
key_addr,
awaited_by)
awaited_by,
recurse_task
)
) {
return -1;
}
@ -666,7 +680,8 @@ parse_task_awaited_by(
struct _Py_DebugOffsets* offsets,
struct _Py_AsyncioModuleDebugOffsets* async_offsets,
uintptr_t task_address,
PyObject *awaited_by
PyObject *awaited_by,
int recurse_task
) {
uintptr_t task_ab_addr;
int err = read_py_ptr(
@ -696,7 +711,9 @@ parse_task_awaited_by(
offsets,
async_offsets,
task_address + async_offsets->asyncio_task_object.task_awaited_by,
awaited_by)
awaited_by,
recurse_task
)
) {
return -1;
}
@ -715,7 +732,9 @@ parse_task_awaited_by(
offsets,
async_offsets,
sub_task,
awaited_by)
awaited_by,
recurse_task
)
) {
return -1;
}
@ -1060,15 +1079,24 @@ append_awaited_by_for_thread(
return -1;
}
PyObject *result_item = PyTuple_New(2);
if (result_item == NULL) {
PyObject* task_id = PyLong_FromUnsignedLongLong(task_addr);
if (task_id == NULL) {
Py_DECREF(tn);
Py_DECREF(current_awaited_by);
return -1;
}
PyTuple_SET_ITEM(result_item, 0, tn); // steals ref
PyTuple_SET_ITEM(result_item, 1, current_awaited_by); // steals ref
PyObject *result_item = PyTuple_New(3);
if (result_item == NULL) {
Py_DECREF(tn);
Py_DECREF(current_awaited_by);
Py_DECREF(task_id);
return -1;
}
PyTuple_SET_ITEM(result_item, 0, task_id); // steals ref
PyTuple_SET_ITEM(result_item, 1, tn); // steals ref
PyTuple_SET_ITEM(result_item, 2, current_awaited_by); // steals ref
if (PyList_Append(result, result_item)) {
Py_DECREF(result_item);
return -1;
@ -1076,7 +1104,7 @@ append_awaited_by_for_thread(
Py_DECREF(result_item);
if (parse_task_awaited_by(handle, debug_offsets, async_offsets,
task_addr, current_awaited_by))
task_addr, current_awaited_by, 0))
{
return -1;
}
@ -1499,7 +1527,7 @@ get_async_stack_trace(PyObject* self, PyObject* args)
if (parse_task_awaited_by(
handle, &local_debug_offsets, &local_async_debug,
running_task_addr, awaited_by)
running_task_addr, awaited_by, 1)
) {
goto result_err;
}
@ -1526,13 +1554,13 @@ static PyMethodDef methods[] = {
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_testexternalinspection",
.m_name = "_remotedebugging",
.m_size = -1,
.m_methods = methods,
};
PyMODINIT_FUNC
PyInit__testexternalinspection(void)
PyInit__remotedebugging(void)
{
PyObject* mod = PyModule_Create(&module);
if (mod == NULL) {

View file

@ -68,7 +68,7 @@
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{4D7C112F-3083-4D9E-9754-9341C14D9B39}</ProjectGuid>
<RootNamespace>_testexternalinspection</RootNamespace>
<RootNamespace>_remotedebugging</RootNamespace>
<Keyword>Win32Proj</Keyword>
<SupportPGO>false</SupportPGO>
</PropertyGroup>
@ -93,7 +93,7 @@
<_ProjectFileVersion>10.0.30319.1</_ProjectFileVersion>
</PropertyGroup>
<ItemGroup>
<ClCompile Include="..\Modules\_testexternalinspection.c" />
<ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc" />

View file

@ -9,7 +9,7 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Modules\_testexternalinspection.c" />
<ClCompile Include="..\Modules\_remotedebuggingmodule.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc">

View file

@ -66,7 +66,7 @@
<!-- pyshellext.dll -->
<Projects Include="pyshellext.vcxproj" />
<!-- Extension modules -->
<ExtensionModules Include="_asyncio;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
<ExtensionModules Include="_asyncio;_remotedebugging;_zoneinfo;_decimal;_elementtree;_multiprocessing;_overlapped;pyexpat;_queue;select;unicodedata;winsound;_uuid;_wmi" />
<ExtensionModules Include="_ctypes" Condition="$(IncludeCTypes)" />
<!-- Extension modules that require external sources -->
<ExternalModules Include="_bz2;_lzma;_sqlite3" />
@ -79,7 +79,7 @@
<ExtensionModules Include="@(ExternalModules->'%(Identity)')" Condition="$(IncludeExternals)" />
<Projects Include="@(ExtensionModules->'%(Identity).vcxproj')" Condition="$(IncludeExtensions)" />
<!-- Test modules -->
<TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testexternalinspection;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
<TestModules Include="_ctypes_test;_testbuffer;_testcapi;_testlimitedcapi;_testinternalcapi;_testembed;_testimportmultiple;_testmultiphase;_testsinglephase;_testconsole;_testclinic;_testclinic_limited" />
<TestModules Include="xxlimited" Condition="'$(Configuration)' == 'Release'" />
<TestModules Include="xxlimited_35" Condition="'$(Configuration)' == 'Release'" />
<Projects Include="@(TestModules->'%(Identity).vcxproj')" Condition="$(IncludeTests)">

View file

@ -81,7 +81,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testclinic", "_testclinic.
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testinternalcapi", "_testinternalcapi.vcxproj", "{900342D7-516A-4469-B1AD-59A66E49A25F}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testexternalinspection", "_testexternalinspection.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_remotedebugging", "_remotedebugging.vcxproj", "{4D7C112F-3083-4D9E-9754-9341C14D9B39}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testimportmultiple", "_testimportmultiple.vcxproj", "{36D0C52C-DF4E-45D0-8BC7-E294C3ABC781}"
EndProject

View file

@ -34,7 +34,7 @@ IGNORE = {
'_testlimitedcapi',
'_testmultiphase',
'_testsinglephase',
'_testexternalinspection',
'_remotedebugging',
'_xxtestfuzz',
'idlelib.idle_test',
'test',

40
configure generated vendored
View file

@ -654,8 +654,8 @@ MODULE__XXTESTFUZZ_FALSE
MODULE__XXTESTFUZZ_TRUE
MODULE_XXSUBTYPE_FALSE
MODULE_XXSUBTYPE_TRUE
MODULE__TESTEXTERNALINSPECTION_FALSE
MODULE__TESTEXTERNALINSPECTION_TRUE
MODULE__REMOTEDEBUGGING_FALSE
MODULE__REMOTEDEBUGGING_TRUE
MODULE__TESTSINGLEPHASE_FALSE
MODULE__TESTSINGLEPHASE_TRUE
MODULE__TESTMULTIPHASE_FALSE
@ -30684,7 +30684,7 @@ case $ac_sys_system in #(
py_cv_module__ctypes_test=n/a
py_cv_module__testexternalinspection=n/a
py_cv_module__remotedebugging=n/a
py_cv_module__testimportmultiple=n/a
py_cv_module__testmultiphase=n/a
py_cv_module__testsinglephase=n/a
@ -33449,44 +33449,44 @@ fi
printf "%s\n" "$py_cv_module__testsinglephase" >&6; }
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _testexternalinspection" >&5
printf %s "checking for stdlib extension module _testexternalinspection... " >&6; }
if test "$py_cv_module__testexternalinspection" != "n/a"
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _remotedebugging" >&5
printf %s "checking for stdlib extension module _remotedebugging... " >&6; }
if test "$py_cv_module__remotedebugging" != "n/a"
then :
if test "$TEST_MODULES" = yes
then :
if true
then :
py_cv_module__testexternalinspection=yes
py_cv_module__remotedebugging=yes
else case e in #(
e) py_cv_module__testexternalinspection=missing ;;
e) py_cv_module__remotedebugging=missing ;;
esac
fi
else case e in #(
e) py_cv_module__testexternalinspection=disabled ;;
e) py_cv_module__remotedebugging=disabled ;;
esac
fi
fi
as_fn_append MODULE_BLOCK "MODULE__TESTEXTERNALINSPECTION_STATE=$py_cv_module__testexternalinspection$as_nl"
if test "x$py_cv_module__testexternalinspection" = xyes
as_fn_append MODULE_BLOCK "MODULE__REMOTEDEBUGGING_STATE=$py_cv_module__remotedebugging$as_nl"
if test "x$py_cv_module__remotedebugging" = xyes
then :
fi
if test "$py_cv_module__testexternalinspection" = yes; then
MODULE__TESTEXTERNALINSPECTION_TRUE=
MODULE__TESTEXTERNALINSPECTION_FALSE='#'
if test "$py_cv_module__remotedebugging" = yes; then
MODULE__REMOTEDEBUGGING_TRUE=
MODULE__REMOTEDEBUGGING_FALSE='#'
else
MODULE__TESTEXTERNALINSPECTION_TRUE='#'
MODULE__TESTEXTERNALINSPECTION_FALSE=
MODULE__REMOTEDEBUGGING_TRUE='#'
MODULE__REMOTEDEBUGGING_FALSE=
fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__testexternalinspection" >&5
printf "%s\n" "$py_cv_module__testexternalinspection" >&6; }
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__remotedebugging" >&5
printf "%s\n" "$py_cv_module__remotedebugging" >&6; }
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module xxsubtype" >&5
@ -34119,8 +34119,8 @@ if test -z "${MODULE__TESTSINGLEPHASE_TRUE}" && test -z "${MODULE__TESTSINGLEPHA
as_fn_error $? "conditional \"MODULE__TESTSINGLEPHASE\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
if test -z "${MODULE__TESTEXTERNALINSPECTION_TRUE}" && test -z "${MODULE__TESTEXTERNALINSPECTION_FALSE}"; then
as_fn_error $? "conditional \"MODULE__TESTEXTERNALINSPECTION\" was never defined.
if test -z "${MODULE__REMOTEDEBUGGING_TRUE}" && test -z "${MODULE__REMOTEDEBUGGING_FALSE}"; then
as_fn_error $? "conditional \"MODULE__REMOTEDEBUGGING\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
if test -z "${MODULE_XXSUBTYPE_TRUE}" && test -z "${MODULE_XXSUBTYPE_FALSE}"; then

View file

@ -7720,7 +7720,7 @@ AS_CASE([$ac_sys_system],
dnl (see Modules/Setup.stdlib.in).
PY_STDLIB_MOD_SET_NA(
[_ctypes_test],
[_testexternalinspection],
[_remotedebugging],
[_testimportmultiple],
[_testmultiphase],
[_testsinglephase],
@ -8082,7 +8082,7 @@ PY_STDLIB_MOD([_testbuffer], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_testimportmultiple], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
PY_STDLIB_MOD([_testmultiphase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
PY_STDLIB_MOD([_testsinglephase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_remotedebugging], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([xxsubtype], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_xxtestfuzz], [test "$TEST_MODULES" = yes])
PY_STDLIB_MOD([_ctypes_test],