Skip to content

Commit 90a54d9

Browse files
committed
Implement, test, and document asyncio.print_call_stack()
1 parent 0fa2067 commit 90a54d9

File tree

3 files changed

+118
-14
lines changed

3 files changed

+118
-14
lines changed

Doc/library/asyncio-stack.rst

+18-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ a suspended *future*.
2323
Capture the async call stack for the current task or the provided
2424
:class:`Task` or :class:`Future`.
2525

26-
The function receives an optional keyword-only *future* argument.
27-
If not passed, the current task will be used. If there's no current task,
28-
the function returns ``None``.
26+
The function recieves an optional keyword-only *future* argument.
27+
If not passed, the current running task will be used. If there's no
28+
current task, the function returns ``None``.
2929

3030
Returns a ``FutureCallStack`` named tuple:
3131

@@ -49,6 +49,17 @@ a suspended *future*.
4949
Where ``coroutine`` is a coroutine object of an awaiting coroutine
5050
or asyncronous generator.
5151

52+
.. function:: print_call_stack(*, future=None, file=None)
53+
54+
Print the async call stack for the current task or the provided
55+
:class:`Task` or :class:`Future`.
56+
57+
The function recieves an optional keyword-only *future* argument.
58+
If not passed, the current running task will be used. If there's no
59+
current task, the function returns ``None``.
60+
61+
If *file* is not specified the function will print to :data:`sys.stdout`.
62+
5263

5364
Low level utility functions
5465
===========================
@@ -70,6 +81,10 @@ the tasks they wrap or control.
7081
:class:`asyncio.Future <Future>` or :class:`asyncio.Task <Task>` or
7182
their subclasses, otherwise the call would have no effect.
7283

84+
A call to ``future_add_to_awaited_by()`` must be followed by an
85+
eventual call to the ``future_discard_from_awaited_by()`` function
86+
with the same arguments.
87+
7388

7489
.. function:: future_discard_from_awaited_by(future, waiter, /)
7590

Lib/asyncio/stack.py

+76-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
__all__ = (
1212
'capture_call_stack',
13+
'print_call_stack',
1314
'FrameCallStackEntry',
1415
'CoroutineCallStackEntry',
1516
'FutureCallStack',
@@ -110,15 +111,15 @@ def capture_call_stack(*, future: any = None) -> FutureCallStack | None:
110111
# Check if we're in a context of a running event loop;
111112
# if yes - check if the passed future is the currently
112113
# running task or not.
113-
if loop is None or future is not tasks.current_task():
114+
if loop is None or future is not tasks.current_task(loop=loop):
114115
return _build_stack_for_future(future)
115116
# else: future is the current task, move on.
116117
else:
117118
if loop is None:
118119
raise RuntimeError(
119120
'capture_call_stack() is called outside of a running '
120121
'event loop and no *future* to introspect was provided')
121-
future = tasks.current_task()
122+
future = tasks.current_task(loop=loop)
122123

123124
if future is None:
124125
# This isn't a generic call stack introspection utility. If we
@@ -158,3 +159,76 @@ def capture_call_stack(*, future: any = None) -> FutureCallStack | None:
158159
awaited_by.append(_build_stack_for_future(parent))
159160

160161
return FutureCallStack(future, call_stack, awaited_by)
162+
163+
164+
def print_call_stack(*, future: any = None, file=None) -> None:
165+
"""Print async call stack for the current task or the provided Future."""
166+
167+
stack = capture_call_stack(future=future)
168+
if stack is None:
169+
return
170+
171+
buf = []
172+
173+
def render_level(st: FutureCallStack, level: int = 0):
174+
def add_line(line: str):
175+
buf.append(level * ' ' + line)
176+
177+
if isinstance(st.future, tasks.Task):
178+
add_line(
179+
f'* Task(name={st.future.get_name()!r}, id=0x{id(st.future):x})'
180+
)
181+
else:
182+
add_line(
183+
f'* Future(id=0x{id(st.future):x})'
184+
)
185+
186+
if st.call_stack:
187+
add_line(
188+
f' + Call stack:'
189+
)
190+
for ste in st.call_stack:
191+
if isinstance(ste, FrameCallStackEntry):
192+
f = ste.frame
193+
add_line(
194+
f' | * {f.f_code.co_qualname}()'
195+
)
196+
add_line(
197+
f' | {f.f_code.co_filename}:{f.f_lineno}'
198+
)
199+
else:
200+
assert isinstance(ste, CoroutineCallStackEntry)
201+
c = ste.coroutine
202+
203+
try:
204+
f = c.cr_frame
205+
code = c.cr_code
206+
tag = 'async'
207+
except AttributeError:
208+
try:
209+
f = c.ag_frame
210+
code = c.ag_code
211+
tag = 'async generator'
212+
except AttributeError:
213+
f = c.gi_frame
214+
code = c.gi_code
215+
tag = 'generator'
216+
217+
add_line(
218+
f' | * {tag} {code.co_qualname}()'
219+
)
220+
add_line(
221+
f' | {f.f_code.co_filename}:{f.f_lineno}'
222+
)
223+
224+
if st.awaited_by:
225+
add_line(
226+
f' + Awaited by:'
227+
)
228+
for fut in st.awaited_by:
229+
render_level(fut, level + 1)
230+
231+
render_level(stack)
232+
rendered = '\n'.join(buf)
233+
234+
print(rendered, file=file)

Lib/test/test_asyncio/test_stack.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import io
23
import unittest
34

45

@@ -37,8 +38,11 @@ def walk(s):
3738

3839
return ret
3940

41+
buf = io.StringIO()
42+
asyncio.print_call_stack(future=fut, file=buf)
43+
4044
stack = asyncio.capture_call_stack(future=fut)
41-
return walk(stack)
45+
return walk(stack), buf.getvalue()
4246

4347

4448
class TestCallStack(unittest.IsolatedAsyncioTestCase):
@@ -73,7 +77,7 @@ async def main():
7377

7478
await main()
7579

76-
self.assertEqual(stack_for_c5, [
80+
self.assertEqual(stack_for_c5[0], [
7781
# task name
7882
'T<c2_root>',
7983
# call stack
@@ -102,6 +106,11 @@ async def main():
102106
]
103107
])
104108

109+
self.assertIn(
110+
'* async TestCallStack.test_stack_tgroup()',
111+
stack_for_c5[1])
112+
113+
105114
async def test_stack_async_gen(self):
106115

107116
stack_for_gen_nested_call = None
@@ -122,7 +131,7 @@ async def main():
122131

123132
await main()
124133

125-
self.assertEqual(stack_for_gen_nested_call, [
134+
self.assertEqual(stack_for_gen_nested_call[0], [
126135
'T<anon>',
127136
[
128137
's capture_test_stack',
@@ -134,6 +143,10 @@ async def main():
134143
[]
135144
])
136145

146+
self.assertIn(
147+
'async generator TestCallStack.test_stack_async_gen.<locals>.gen()',
148+
stack_for_gen_nested_call[1])
149+
137150
async def test_stack_gather(self):
138151

139152
stack_for_deep = None
@@ -155,7 +168,7 @@ async def main():
155168

156169
await main()
157170

158-
self.assertEqual(stack_for_deep, [
171+
self.assertEqual(stack_for_deep[0], [
159172
'T<anon>',
160173
['s capture_test_stack', 'a deep', 'a c1'],
161174
[
@@ -181,7 +194,7 @@ async def main():
181194

182195
await main()
183196

184-
self.assertEqual(stack_for_shield, [
197+
self.assertEqual(stack_for_shield[0], [
185198
'T<anon>',
186199
['s capture_test_stack', 'a deep', 'a c1'],
187200
[
@@ -208,7 +221,7 @@ async def main():
208221

209222
await main()
210223

211-
self.assertEqual(stack_for_inner, [
224+
self.assertEqual(stack_for_inner[0], [
212225
'T<anon>',
213226
['s capture_test_stack', 'a inner', 'a c1'],
214227
[
@@ -248,7 +261,7 @@ async def main(t1, t2):
248261
await t1
249262
await t2
250263

251-
self.assertEqual(stack_for_inner, [
264+
self.assertEqual(stack_for_inner[0], [
252265
'T<anon>',
253266
['s capture_test_stack', 'a inner', 'a c1'],
254267
[
@@ -279,7 +292,7 @@ async def main():
279292

280293
await main()
281294

282-
self.assertEqual(stack_for_inner, [
295+
self.assertEqual(stack_for_inner[0], [
283296
'T<there there>',
284297
['s capture_test_stack', 'a inner', 'a c1'],
285298
[['T<anon>', ['a c2', 'a main', 'a test_stack_task'], []]]
@@ -316,7 +329,7 @@ async def main():
316329

317330
await main()
318331

319-
self.assertEqual(stack_for_fut,
332+
self.assertEqual(stack_for_fut[0],
320333
['F',
321334
[],
322335
[
@@ -330,3 +343,5 @@ async def main():
330343
],
331344
]]
332345
)
346+
347+
self.assertTrue(stack_for_fut[1].startswith('* Future(id='))

0 commit comments

Comments
 (0)