diff options
-rw-r--r-- | Doc/whatsnew/3.14.rst | 99 | ||||
-rw-r--r-- | Lib/asyncio/__main__.py | 32 | ||||
-rw-r--r-- | Lib/asyncio/tools.py | 212 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_tools.py | 839 | ||||
-rw-r--r-- | Lib/test/test_external_inspection.py | 45 | ||||
-rw-r--r-- | Lib/test/test_sys.py | 2 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst | 6 | ||||
-rw-r--r-- | Modules/Setup | 2 | ||||
-rw-r--r-- | Modules/Setup.stdlib.in | 2 | ||||
-rw-r--r-- | Modules/_remotedebuggingmodule.c (renamed from Modules/_testexternalinspection.c) | 100 | ||||
-rw-r--r-- | PCbuild/_remotedebugging.vcxproj (renamed from PCbuild/_testexternalinspection.vcxproj) | 4 | ||||
-rw-r--r-- | PCbuild/_remotedebugging.vcxproj.filters (renamed from PCbuild/_testexternalinspection.vcxproj.filters) | 2 | ||||
-rw-r--r-- | PCbuild/pcbuild.proj | 4 | ||||
-rw-r--r-- | PCbuild/pcbuild.sln | 2 | ||||
-rw-r--r-- | Tools/build/generate_stdlib_module_names.py | 2 | ||||
-rwxr-xr-x | configure | 40 | ||||
-rw-r--r-- | configure.ac | 4 |
17 files changed, 1308 insertions, 89 deletions
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 87c31d32e22..81581b30d21 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 69f5a30cfe5..7d980bc401a 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -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'): diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py new file mode 100644 index 00000000000..16440b594ad --- /dev/null +++ b/Lib/asyncio/tools.py @@ -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)) diff --git a/Lib/test/test_asyncio/test_tools.py b/Lib/test/test_asyncio/test_tools.py new file mode 100644 index 00000000000..2caf56172c9 --- /dev/null +++ b/Lib/test/test_asyncio/test_tools.py @@ -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) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index aa05db972f0..4e82f567e1f 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -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__": diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 56413d00823..10c3e0e9a1d 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -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 diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst new file mode 100644 index 00000000000..1d45868b7b2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-03-19-04-03.gh-issue-91048.S8QWSw.rst @@ -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. diff --git a/Modules/Setup b/Modules/Setup index 65c22d48ba0..c3e0d9eb934 100644 --- a/Modules/Setup +++ b/Modules/Setup @@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH) #_testcapi _testcapimodule.c #_testimportmultiple _testimportmultiple.c #_testmultiphase _testmultiphase.c -#_testexternalinspection _testexternalinspection.c +#_remotedebugging _remotedebuggingmodule.c #_testsinglephase _testsinglephase.c # --- diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 33e60f37d19..be4fb513e59 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -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. diff --git a/Modules/_testexternalinspection.c b/Modules/_remotedebuggingmodule.c index b65c5821443..0e055ae1604 100644 --- a/Modules/_testexternalinspection.c +++ b/Modules/_remotedebuggingmodule.c @@ -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); + PyObject* task_id = PyLong_FromUnsignedLongLong(task_addr); + if (task_id == NULL) { + Py_DECREF(tn); + Py_DECREF(current_awaited_by); + return -1; + } + + 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, tn); // steals ref - PyTuple_SET_ITEM(result_item, 1, current_awaited_by); // steals ref + 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) { diff --git a/PCbuild/_testexternalinspection.vcxproj b/PCbuild/_remotedebugging.vcxproj index d5f347ecfec..a16079f7c6c 100644 --- a/PCbuild/_testexternalinspection.vcxproj +++ b/PCbuild/_remotedebugging.vcxproj @@ -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" /> diff --git a/PCbuild/_testexternalinspection.vcxproj.filters b/PCbuild/_remotedebugging.vcxproj.filters index feb4343e5c2..888e2cd478a 100644 --- a/PCbuild/_testexternalinspection.vcxproj.filters +++ b/PCbuild/_remotedebugging.vcxproj.filters @@ -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"> diff --git a/PCbuild/pcbuild.proj b/PCbuild/pcbuild.proj index 1bf430e03de..eec213d7bac 100644 --- a/PCbuild/pcbuild.proj +++ b/PCbuild/pcbuild.proj @@ -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)"> diff --git a/PCbuild/pcbuild.sln b/PCbuild/pcbuild.sln index 803bb149c90..d2bfb9472b1 100644 --- a/PCbuild/pcbuild.sln +++ b/PCbuild/pcbuild.sln @@ -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 diff --git a/Tools/build/generate_stdlib_module_names.py b/Tools/build/generate_stdlib_module_names.py index 9873890837f..761eecba96f 100644 --- a/Tools/build/generate_stdlib_module_names.py +++ b/Tools/build/generate_stdlib_module_names.py @@ -34,7 +34,7 @@ IGNORE = { '_testlimitedcapi', '_testmultiphase', '_testsinglephase', - '_testexternalinspection', + '_remotedebugging', '_xxtestfuzz', 'idlelib.idle_test', 'test', diff --git a/configure b/configure index 7dbb35f9f45..3b74554d5a2 100755 --- a/configure +++ b/configure @@ -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 diff --git a/configure.ac b/configure.ac index 65f265045ba..ed5c65ecbcc 100644 --- a/configure.ac +++ b/configure.ac @@ -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], |