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 <aeros167@gmail.com>
This commit is contained in:
parent
0b9c4c6fcf
commit
42d873c63a
|
@ -1,6 +1,7 @@
|
||||||
__all__ = ()
|
__all__ = ()
|
||||||
|
|
||||||
import reprlib
|
import reprlib
|
||||||
|
from _thread import get_ident
|
||||||
|
|
||||||
from . import format_helpers
|
from . import format_helpers
|
||||||
|
|
||||||
|
@ -41,6 +42,16 @@ def _format_callbacks(cb):
|
||||||
return f'cb=[{cb}]'
|
return f'cb=[{cb}]'
|
||||||
|
|
||||||
|
|
||||||
|
# bpo-42183: _repr_running is needed for repr protection
|
||||||
|
# when a Future or Task result contains itself directly or indirectly.
|
||||||
|
# The logic is borrowed from @reprlib.recursive_repr decorator.
|
||||||
|
# Unfortunately, the direct decorator usage is impossible because of
|
||||||
|
# AttributeError: '_asyncio.Task' object has no attribute '__module__' error.
|
||||||
|
#
|
||||||
|
# After fixing this thing we can return to the decorator based approach.
|
||||||
|
_repr_running = set()
|
||||||
|
|
||||||
|
|
||||||
def _future_repr_info(future):
|
def _future_repr_info(future):
|
||||||
# (Future) -> str
|
# (Future) -> str
|
||||||
"""helper function for Future.__repr__"""
|
"""helper function for Future.__repr__"""
|
||||||
|
@ -49,9 +60,17 @@ def _future_repr_info(future):
|
||||||
if future._exception is not None:
|
if future._exception is not None:
|
||||||
info.append(f'exception={future._exception!r}')
|
info.append(f'exception={future._exception!r}')
|
||||||
else:
|
else:
|
||||||
# use reprlib to limit the length of the output, especially
|
key = id(future), get_ident()
|
||||||
# for very long strings
|
if key in _repr_running:
|
||||||
result = reprlib.repr(future._result)
|
result = '...'
|
||||||
|
else:
|
||||||
|
_repr_running.add(key)
|
||||||
|
try:
|
||||||
|
# use reprlib to limit the length of the output, especially
|
||||||
|
# for very long strings
|
||||||
|
result = reprlib.repr(future._result)
|
||||||
|
finally:
|
||||||
|
_repr_running.discard(key)
|
||||||
info.append(f'result={result}')
|
info.append(f'result={result}')
|
||||||
if future._callbacks:
|
if future._callbacks:
|
||||||
info.append(_format_callbacks(future._callbacks))
|
info.append(_format_callbacks(future._callbacks))
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# IsolatedAsyncioTestCase based tests
|
||||||
|
import asyncio
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class FutureTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_recursive_repr_for_pending_tasks(self):
|
||||||
|
# The call crashes if the guard for recursive call
|
||||||
|
# in base_futures:_future_repr_info is absent
|
||||||
|
# See Also: https://bugs.python.org/issue42183
|
||||||
|
|
||||||
|
async def func():
|
||||||
|
return asyncio.all_tasks()
|
||||||
|
|
||||||
|
# The repr() call should not raise RecursiveError at first.
|
||||||
|
# The check for returned string is not very reliable but
|
||||||
|
# exact comparison for the whole string is even weaker.
|
||||||
|
self.assertIn('...', repr(await asyncio.wait_for(func(), timeout=10)))
|
|
@ -0,0 +1,4 @@
|
||||||
|
Fix a stack overflow error for asyncio Task or Future repr().
|
||||||
|
|
||||||
|
The overflow occurs under some circumstances when a Task or Future
|
||||||
|
recursively returns itself.
|
Loading…
Reference in New Issue