Skip to content

Commit 42d873c

Browse files
asvetlovaeros
andauthored
bpo-42183: Fix a stack overflow error for asyncio Task or Future repr() (GH-23020)
The overflow occurs under some circumstances when a task or future recursively returns itself. Co-authored-by: Kyle Stanley <[email protected]>
1 parent 0b9c4c6 commit 42d873c

File tree

3 files changed

+44
-3
lines changed

3 files changed

+44
-3
lines changed

Lib/asyncio/base_futures.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
__all__ = ()
22

33
import reprlib
4+
from _thread import get_ident
45

56
from . import format_helpers
67

@@ -41,6 +42,16 @@ def format_cb(callback):
4142
return f'cb=[{cb}]'
4243

4344

45+
# bpo-42183: _repr_running is needed for repr protection
46+
# when a Future or Task result contains itself directly or indirectly.
47+
# The logic is borrowed from @reprlib.recursive_repr decorator.
48+
# Unfortunately, the direct decorator usage is impossible because of
49+
# AttributeError: '_asyncio.Task' object has no attribute '__module__' error.
50+
#
51+
# After fixing this thing we can return to the decorator based approach.
52+
_repr_running = set()
53+
54+
4455
def _future_repr_info(future):
4556
# (Future) -> str
4657
"""helper function for Future.__repr__"""
@@ -49,9 +60,17 @@ def _future_repr_info(future):
4960
if future._exception is not None:
5061
info.append(f'exception={future._exception!r}')
5162
else:
52-
# use reprlib to limit the length of the output, especially
53-
# for very long strings
54-
result = reprlib.repr(future._result)
63+
key = id(future), get_ident()
64+
if key in _repr_running:
65+
result = '...'
66+
else:
67+
_repr_running.add(key)
68+
try:
69+
# use reprlib to limit the length of the output, especially
70+
# for very long strings
71+
result = reprlib.repr(future._result)
72+
finally:
73+
_repr_running.discard(key)
5574
info.append(f'result={result}')
5675
if future._callbacks:
5776
info.append(_format_callbacks(future._callbacks))
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# IsolatedAsyncioTestCase based tests
2+
import asyncio
3+
import unittest
4+
5+
6+
class FutureTests(unittest.IsolatedAsyncioTestCase):
7+
async def test_recursive_repr_for_pending_tasks(self):
8+
# The call crashes if the guard for recursive call
9+
# in base_futures:_future_repr_info is absent
10+
# See Also: https://bugs.python.org/issue42183
11+
12+
async def func():
13+
return asyncio.all_tasks()
14+
15+
# The repr() call should not raise RecursiveError at first.
16+
# The check for returned string is not very reliable but
17+
# exact comparison for the whole string is even weaker.
18+
self.assertIn('...', repr(await asyncio.wait_for(func(), timeout=10)))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix a stack overflow error for asyncio Task or Future repr().
2+
3+
The overflow occurs under some circumstances when a Task or Future
4+
recursively returns itself.

0 commit comments

Comments
 (0)