@@ -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