Skip to content

Commit d6d943f

Browse files
committed
Address remaining suggestions of Andrew Svetlov
1 parent ce332d9 commit d6d943f

File tree

2 files changed

+64
-23
lines changed

2 files changed

+64
-23
lines changed

Doc/library/asyncio-graph.rst

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
.. _asyncio-graph:
55

6-
===================
7-
Stack Introspection
8-
===================
6+
========================
7+
Call Graph Introspection
8+
========================
99

1010
**Source code:** :source:`Lib/asyncio/graph.py`
1111

@@ -20,20 +20,31 @@ and debuggers.
2020
.. versionadded:: next
2121

2222

23-
.. function:: print_call_graph(future=None, /, *, file=None, depth=1)
23+
.. function:: print_call_graph(future=None, /, *, file=None, depth=1, limit=None)
2424

2525
Print the async call graph for the current task or the provided
2626
:class:`Task` or :class:`Future`.
2727

28+
This function prints entries starting from the currently executing frame,
29+
i.e. the top frame, and going down towards the invocation point.
30+
2831
The function receives an optional *future* argument.
29-
If not passed, the current running task will be used. If there's no
30-
current task, the function returns ``None``.
32+
If not passed, the current running task will be used.
3133

3234
If the function is called on *the current task*, the optional
3335
keyword-only *depth* argument can be used to skip the specified
3436
number of frames from top of the stack.
3537

36-
If *file* is omitted or ``None``, the function will print to :data:`sys.stdout`.
38+
If the optional keyword-only *limit* argument is provided, each call stack
39+
in the resulting graph is truncated to include at most ``abs(limit)``
40+
entries. If *limit* is positive, the entries left are the closest to
41+
the invocation point. If *limit* is negative, the topmost entries are
42+
left. If *limit* is omitted or ``None``, all entries are present.
43+
If *limit* is ``0``, the call stack is not printed at all, only
44+
"awaited by" information is printed.
45+
46+
If *file* is omitted or ``None``, the function will print
47+
to :data:`sys.stdout`.
3748

3849
**Example:**
3950

@@ -63,11 +74,14 @@ and debuggers.
6374
| File 'taskgroups.py', line 107, in async TaskGroup.__aexit__()
6475
| File 't2.py', line 7, in async main()
6576

66-
.. function:: format_call_graph(future=None, /, *, depth=1)
77+
.. function:: format_call_graph(future=None, /, *, depth=1, limit=None)
6778

6879
Like :func:`print_call_graph`, but returns a string.
80+
If *future* is ``None`` and there's no current task,
81+
the function returns an empty string.
82+
6983

70-
.. function:: capture_call_graph(future=None, /, *, depth=1)
84+
.. function:: capture_call_graph(future=None, /, *, depth=1, limit=None)
7185

7286
Capture the async call graph for the current task or the provided
7387
:class:`Task` or :class:`Future`.

Lib/asyncio/graph.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ class FutureCallGraph:
3737
awaited_by: tuple["FutureCallGraph", ...]
3838

3939

40-
def _build_graph_for_future(future: futures.Future) -> FutureCallGraph:
40+
def _build_graph_for_future(
41+
future: futures.Future,
42+
*,
43+
limit: int | None = None,
44+
) -> FutureCallGraph:
4145
if not isinstance(future, futures.Future):
4246
raise TypeError(
4347
f"{future!r} object does not appear to be compatible "
@@ -46,7 +50,7 @@ def _build_graph_for_future(future: futures.Future) -> FutureCallGraph:
4650

4751
coro = None
4852
if get_coro := getattr(future, 'get_coro', None):
49-
coro = get_coro()
53+
coro = get_coro() if limit != 0 else None
5054

5155
st: list[FrameCallGraphEntry] = []
5256
awaited_by: list[FutureCallGraph] = []
@@ -65,8 +69,13 @@ def _build_graph_for_future(future: futures.Future) -> FutureCallGraph:
6569

6670
if future._asyncio_awaited_by:
6771
for parent in future._asyncio_awaited_by:
68-
awaited_by.append(_build_graph_for_future(parent))
72+
awaited_by.append(_build_graph_for_future(parent, limit=limit))
6973

74+
if limit is not None:
75+
if limit > 0:
76+
st = st[:limit]
77+
elif limit < 0:
78+
st = st[limit:]
7079
st.reverse()
7180
return FutureCallGraph(future, tuple(st), tuple(awaited_by))
7281

@@ -76,8 +85,9 @@ def capture_call_graph(
7685
/,
7786
*,
7887
depth: int = 1,
88+
limit: int | None = None,
7989
) -> FutureCallGraph | None:
80-
"""Capture async call graph for the current task or the provided Future.
90+
"""Capture the async call graph for the current task or the provided Future.
8191
8292
The graph is represented with three data structures:
8393
@@ -95,13 +105,21 @@ def capture_call_graph(
95105
Where 'frame' is a frame object of a regular Python function
96106
in the call stack.
97107
98-
Receives an optional "future" argument. If not passed,
108+
Receives an optional 'future' argument. If not passed,
99109
the current task will be used. If there's no current task, the function
100110
returns None.
101111
102112
If "capture_call_graph()" is introspecting *the current task*, the
103-
optional keyword-only "depth" argument can be used to skip the specified
113+
optional keyword-only 'depth' argument can be used to skip the specified
104114
number of frames from top of the stack.
115+
116+
If the optional keyword-only 'limit' argument is provided, each call stack
117+
in the resulting graph is truncated to include at most ``abs(limit)``
118+
entries. If 'limit' is positive, the entries left are the closest to
119+
the invocation point. If 'limit' is negative, the topmost entries are
120+
left. If 'limit' is omitted or None, all entries are present.
121+
If 'limit' is 0, the call stack is not captured at all, only
122+
"awaited by" information is present.
105123
"""
106124

107125
loop = events._get_running_loop()
@@ -111,7 +129,7 @@ def capture_call_graph(
111129
# if yes - check if the passed future is the currently
112130
# running task or not.
113131
if loop is None or future is not tasks.current_task(loop=loop):
114-
return _build_graph_for_future(future)
132+
return _build_graph_for_future(future, limit=limit)
115133
# else: future is the current task, move on.
116134
else:
117135
if loop is None:
@@ -134,7 +152,7 @@ def capture_call_graph(
134152

135153
call_stack: list[FrameCallGraphEntry] = []
136154

137-
f = sys._getframe(depth)
155+
f = sys._getframe(depth) if limit != 0 else None
138156
try:
139157
while f is not None:
140158
is_async = f.f_generator is not None
@@ -153,7 +171,14 @@ def capture_call_graph(
153171
awaited_by = []
154172
if future._asyncio_awaited_by:
155173
for parent in future._asyncio_awaited_by:
156-
awaited_by.append(_build_graph_for_future(parent))
174+
awaited_by.append(_build_graph_for_future(parent, limit=limit))
175+
176+
if limit is not None:
177+
limit *= -1
178+
if limit > 0:
179+
call_stack = call_stack[:limit]
180+
elif limit < 0:
181+
call_stack = call_stack[limit:]
157182

158183
return FutureCallGraph(future, tuple(call_stack), tuple(awaited_by))
159184

@@ -163,8 +188,9 @@ def format_call_graph(
163188
/,
164189
*,
165190
depth: int = 1,
191+
limit: int | None = None,
166192
) -> str:
167-
"""Return async call graph as a string for `future`.
193+
"""Return the async call graph as a string for `future`.
168194
169195
If `future` is not provided, format the call graph for the current task.
170196
"""
@@ -226,9 +252,9 @@ def add_line(line: str) -> None:
226252
for fut in st.awaited_by:
227253
render_level(fut, buf, level + 1)
228254

229-
graph = capture_call_graph(future, depth=depth + 1)
255+
graph = capture_call_graph(future, depth=depth + 1, limit=limit)
230256
if graph is None:
231-
return
257+
return ""
232258

233259
try:
234260
buf: list[str] = []
@@ -245,6 +271,7 @@ def print_call_graph(
245271
*,
246272
file: typing.TextIO | None = None,
247273
depth: int = 1,
274+
limit: int | None = None,
248275
) -> None:
249-
"""Print async call graph for the current task or the provided Future."""
250-
print(format_call_graph(future, depth=depth), file=file)
276+
"""Print the async call graph for the current task or the provided Future."""
277+
print(format_call_graph(future, depth=depth, limit=limit), file=file)

0 commit comments

Comments
 (0)