@@ -37,7 +37,11 @@ class FutureCallGraph:
37
37
awaited_by : tuple ["FutureCallGraph" , ...]
38
38
39
39
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 :
41
45
if not isinstance (future , futures .Future ):
42
46
raise TypeError (
43
47
f"{ future !r} object does not appear to be compatible "
@@ -46,7 +50,7 @@ def _build_graph_for_future(future: futures.Future) -> FutureCallGraph:
46
50
47
51
coro = None
48
52
if get_coro := getattr (future , 'get_coro' , None ):
49
- coro = get_coro ()
53
+ coro = get_coro () if limit != 0 else None
50
54
51
55
st : list [FrameCallGraphEntry ] = []
52
56
awaited_by : list [FutureCallGraph ] = []
@@ -65,8 +69,13 @@ def _build_graph_for_future(future: futures.Future) -> FutureCallGraph:
65
69
66
70
if future ._asyncio_awaited_by :
67
71
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 ))
69
73
74
+ if limit is not None :
75
+ if limit > 0 :
76
+ st = st [:limit ]
77
+ elif limit < 0 :
78
+ st = st [limit :]
70
79
st .reverse ()
71
80
return FutureCallGraph (future , tuple (st ), tuple (awaited_by ))
72
81
@@ -76,8 +85,9 @@ def capture_call_graph(
76
85
/ ,
77
86
* ,
78
87
depth : int = 1 ,
88
+ limit : int | None = None ,
79
89
) -> 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.
81
91
82
92
The graph is represented with three data structures:
83
93
@@ -95,13 +105,21 @@ def capture_call_graph(
95
105
Where 'frame' is a frame object of a regular Python function
96
106
in the call stack.
97
107
98
- Receives an optional " future" argument. If not passed,
108
+ Receives an optional ' future' argument. If not passed,
99
109
the current task will be used. If there's no current task, the function
100
110
returns None.
101
111
102
112
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
104
114
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.
105
123
"""
106
124
107
125
loop = events ._get_running_loop ()
@@ -111,7 +129,7 @@ def capture_call_graph(
111
129
# if yes - check if the passed future is the currently
112
130
# running task or not.
113
131
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 )
115
133
# else: future is the current task, move on.
116
134
else :
117
135
if loop is None :
@@ -134,7 +152,7 @@ def capture_call_graph(
134
152
135
153
call_stack : list [FrameCallGraphEntry ] = []
136
154
137
- f = sys ._getframe (depth )
155
+ f = sys ._getframe (depth ) if limit != 0 else None
138
156
try :
139
157
while f is not None :
140
158
is_async = f .f_generator is not None
@@ -153,7 +171,14 @@ def capture_call_graph(
153
171
awaited_by = []
154
172
if future ._asyncio_awaited_by :
155
173
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 :]
157
182
158
183
return FutureCallGraph (future , tuple (call_stack ), tuple (awaited_by ))
159
184
@@ -163,8 +188,9 @@ def format_call_graph(
163
188
/ ,
164
189
* ,
165
190
depth : int = 1 ,
191
+ limit : int | None = None ,
166
192
) -> str :
167
- """Return async call graph as a string for `future`.
193
+ """Return the async call graph as a string for `future`.
168
194
169
195
If `future` is not provided, format the call graph for the current task.
170
196
"""
@@ -226,9 +252,9 @@ def add_line(line: str) -> None:
226
252
for fut in st .awaited_by :
227
253
render_level (fut , buf , level + 1 )
228
254
229
- graph = capture_call_graph (future , depth = depth + 1 )
255
+ graph = capture_call_graph (future , depth = depth + 1 , limit = limit )
230
256
if graph is None :
231
- return
257
+ return ""
232
258
233
259
try :
234
260
buf : list [str ] = []
@@ -245,6 +271,7 @@ def print_call_graph(
245
271
* ,
246
272
file : typing .TextIO | None = None ,
247
273
depth : int = 1 ,
274
+ limit : int | None = None ,
248
275
) -> 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