Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 80f53aa

Browse files
committed
asyncio, Tulip issue 137: In debug mode, save traceback where Future, Task and
Handle objects are created. Pass the traceback to call_exception_handler() in the 'source_traceback' key. The traceback is truncated to hide internal calls in asyncio, show only the traceback from user code. Add tests for the new source_traceback, and a test for the 'Future/Task exception was never retrieved' log.
1 parent bbd96c6 commit 80f53aa

8 files changed

Lines changed: 180 additions & 26 deletions

File tree

Lib/asyncio/base_events.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import socket
2323
import subprocess
24+
import traceback
2425
import time
2526
import os
2627
import sys
@@ -290,7 +291,10 @@ def call_later(self, delay, callback, *args):
290291
Any positional arguments after the callback will be passed to
291292
the callback when it is called.
292293
"""
293-
return self.call_at(self.time() + delay, callback, *args)
294+
timer = self.call_at(self.time() + delay, callback, *args)
295+
if timer._source_traceback:
296+
del timer._source_traceback[-1]
297+
return timer
294298

295299
def call_at(self, when, callback, *args):
296300
"""Like call_later(), but uses an absolute time."""
@@ -299,6 +303,8 @@ def call_at(self, when, callback, *args):
299303
if self._debug:
300304
self._assert_is_current_event_loop()
301305
timer = events.TimerHandle(when, callback, args, self)
306+
if timer._source_traceback:
307+
del timer._source_traceback[-1]
302308
heapq.heappush(self._scheduled, timer)
303309
return timer
304310

@@ -312,14 +318,19 @@ def call_soon(self, callback, *args):
312318
Any positional arguments after the callback will be passed to
313319
the callback when it is called.
314320
"""
315-
return self._call_soon(callback, args, check_loop=True)
321+
handle = self._call_soon(callback, args, check_loop=True)
322+
if handle._source_traceback:
323+
del handle._source_traceback[-1]
324+
return handle
316325

317326
def _call_soon(self, callback, args, check_loop):
318327
if tasks.iscoroutinefunction(callback):
319328
raise TypeError("coroutines cannot be used with call_soon()")
320329
if self._debug and check_loop:
321330
self._assert_is_current_event_loop()
322331
handle = events.Handle(callback, args, self)
332+
if handle._source_traceback:
333+
del handle._source_traceback[-1]
323334
self._ready.append(handle)
324335
return handle
325336

@@ -344,6 +355,8 @@ def _assert_is_current_event_loop(self):
344355
def call_soon_threadsafe(self, callback, *args):
345356
"""Like call_soon(), but thread safe."""
346357
handle = self._call_soon(callback, args, check_loop=False)
358+
if handle._source_traceback:
359+
del handle._source_traceback[-1]
347360
self._write_to_self()
348361
return handle
349362

@@ -757,7 +770,14 @@ def default_exception_handler(self, context):
757770
for key in sorted(context):
758771
if key in {'message', 'exception'}:
759772
continue
760-
log_lines.append('{}: {!r}'.format(key, context[key]))
773+
value = context[key]
774+
if key == 'source_traceback':
775+
tb = ''.join(traceback.format_list(value))
776+
value = 'Object created at (most recent call last):\n'
777+
value += tb.rstrip()
778+
else:
779+
value = repr(value)
780+
log_lines.append('{}: {}'.format(key, value))
761781

762782
logger.error('\n'.join(log_lines), exc_info=exc_info)
763783

Lib/asyncio/events.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import functools
1212
import inspect
1313
import subprocess
14+
import traceback
1415
import threading
1516
import socket
1617
import sys
@@ -66,14 +67,19 @@ def _format_callback(func, args, suffix=''):
6667
class Handle:
6768
"""Object returned by callback registration methods."""
6869

69-
__slots__ = ['_callback', '_args', '_cancelled', '_loop', '__weakref__']
70+
__slots__ = ('_callback', '_args', '_cancelled', '_loop',
71+
'_source_traceback', '__weakref__')
7072

7173
def __init__(self, callback, args, loop):
7274
assert not isinstance(callback, Handle), 'A Handle is not a callback'
7375
self._loop = loop
7476
self._callback = callback
7577
self._args = args
7678
self._cancelled = False
79+
if self._loop.get_debug():
80+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
81+
else:
82+
self._source_traceback = None
7783

7884
def __repr__(self):
7985
info = []
@@ -91,11 +97,14 @@ def _run(self):
9197
except Exception as exc:
9298
cb = _format_callback(self._callback, self._args)
9399
msg = 'Exception in callback {}'.format(cb)
94-
self._loop.call_exception_handler({
100+
context = {
95101
'message': msg,
96102
'exception': exc,
97103
'handle': self,
98-
})
104+
}
105+
if self._source_traceback:
106+
context['source_traceback'] = self._source_traceback
107+
self._loop.call_exception_handler(context)
99108
self = None # Needed to break cycles when an exception occurs.
100109

101110

@@ -107,7 +116,8 @@ class TimerHandle(Handle):
107116
def __init__(self, when, callback, args, loop):
108117
assert when is not None
109118
super().__init__(callback, args, loop)
110-
119+
if self._source_traceback:
120+
del self._source_traceback[-1]
111121
self._when = when
112122

113123
def __repr__(self):

Lib/asyncio/futures.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ class itself, but instead to have a reference to a helper object
8282
in a discussion about closing files when they are collected.
8383
"""
8484

85-
__slots__ = ['exc', 'tb', 'loop']
85+
__slots__ = ('loop', 'source_traceback', 'exc', 'tb')
8686

87-
def __init__(self, exc, loop):
88-
self.loop = loop
87+
def __init__(self, future, exc):
88+
self.loop = future._loop
89+
self.source_traceback = future._source_traceback
8990
self.exc = exc
9091
self.tb = None
9192

@@ -102,11 +103,12 @@ def clear(self):
102103

103104
def __del__(self):
104105
if self.tb:
105-
msg = 'Future/Task exception was never retrieved:\n{tb}'
106-
context = {
107-
'message': msg.format(tb=''.join(self.tb)),
108-
}
109-
self.loop.call_exception_handler(context)
106+
msg = 'Future/Task exception was never retrieved'
107+
if self.source_traceback:
108+
msg += '\nFuture/Task created at (most recent call last):\n'
109+
msg += ''.join(traceback.format_list(self.source_traceback))
110+
msg += ''.join(self.tb).rstrip()
111+
self.loop.call_exception_handler({'message': msg})
110112

111113

112114
class Future:
@@ -149,6 +151,10 @@ def __init__(self, *, loop=None):
149151
else:
150152
self._loop = loop
151153
self._callbacks = []
154+
if self._loop.get_debug():
155+
self._source_traceback = traceback.extract_stack(sys._getframe(1))
156+
else:
157+
self._source_traceback = None
152158

153159
def _format_callbacks(self):
154160
cb = self._callbacks
@@ -196,10 +202,13 @@ def __del__(self):
196202
return
197203
exc = self._exception
198204
context = {
199-
'message': 'Future/Task exception was never retrieved',
205+
'message': ('%s exception was never retrieved'
206+
% self.__class__.__name__),
200207
'exception': exc,
201208
'future': self,
202209
}
210+
if self._source_traceback:
211+
context['source_traceback'] = self._source_traceback
203212
self._loop.call_exception_handler(context)
204213

205214
def cancel(self):
@@ -335,7 +344,7 @@ def set_exception(self, exception):
335344
if _PY34:
336345
self._log_traceback = True
337346
else:
338-
self._tb_logger = _TracebackLogger(exception, self._loop)
347+
self._tb_logger = _TracebackLogger(self, exception)
339348
# Arrange for the logger to be activated after all callbacks
340349
# have had a chance to call result() or exception().
341350
self._loop.call_soon(self._tb_logger.activate)

Lib/asyncio/tasks.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ def all_tasks(cls, loop=None):
195195
def __init__(self, coro, *, loop=None):
196196
assert iscoroutine(coro), repr(coro) # Not a coroutine function!
197197
super().__init__(loop=loop)
198+
if self._source_traceback:
199+
del self._source_traceback[-1]
198200
self._coro = iter(coro) # Use the iterator just in case.
199201
self._fut_waiter = None
200202
self._must_cancel = False
@@ -207,10 +209,13 @@ def __init__(self, coro, *, loop=None):
207209
if _PY34:
208210
def __del__(self):
209211
if self._state == futures._PENDING:
210-
self._loop.call_exception_handler({
212+
context = {
211213
'task': self,
212214
'message': 'Task was destroyed but it is pending!',
213-
})
215+
}
216+
if self._source_traceback:
217+
context['source_traceback'] = self._source_traceback
218+
self._loop.call_exception_handler(context)
214219
futures.Future.__del__(self)
215220

216221
def __repr__(self):
@@ -620,7 +625,10 @@ def async(coro_or_future, *, loop=None):
620625
raise ValueError('loop argument must agree with Future')
621626
return coro_or_future
622627
elif iscoroutine(coro_or_future):
623-
return Task(coro_or_future, loop=loop)
628+
task = Task(coro_or_future, loop=loop)
629+
if task._source_traceback:
630+
del task._source_traceback[-1]
631+
return task
624632
else:
625633
raise TypeError('A Future or coroutine is required')
626634

Lib/test/test_asyncio/test_base_events.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,19 +406,22 @@ def zero_error():
406406
1/0
407407

408408
def run_loop():
409-
self.loop.call_soon(zero_error)
409+
handle = self.loop.call_soon(zero_error)
410410
self.loop._run_once()
411+
return handle
411412

413+
self.loop.set_debug(True)
412414
self.loop._process_events = mock.Mock()
413415

414416
mock_handler = mock.Mock()
415417
self.loop.set_exception_handler(mock_handler)
416-
run_loop()
418+
handle = run_loop()
417419
mock_handler.assert_called_with(self.loop, {
418420
'exception': MOCK_ANY,
419421
'message': test_utils.MockPattern(
420422
'Exception in callback.*zero_error'),
421-
'handle': MOCK_ANY,
423+
'handle': handle,
424+
'source_traceback': handle._source_traceback,
422425
})
423426
mock_handler.reset_mock()
424427

Lib/test/test_asyncio/test_events.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,10 +1751,11 @@ def noop(*args):
17511751
pass
17521752

17531753

1754-
class HandleTests(unittest.TestCase):
1754+
class HandleTests(test_utils.TestCase):
17551755

17561756
def setUp(self):
1757-
self.loop = None
1757+
self.loop = mock.Mock()
1758+
self.loop.get_debug.return_value = True
17581759

17591760
def test_handle(self):
17601761
def callback(*args):
@@ -1789,7 +1790,8 @@ def callback():
17891790
self.loop.call_exception_handler.assert_called_with({
17901791
'message': test_utils.MockPattern('Exception in callback.*'),
17911792
'exception': mock.ANY,
1792-
'handle': h
1793+
'handle': h,
1794+
'source_traceback': h._source_traceback,
17931795
})
17941796

17951797
def test_handle_weakref(self):
@@ -1837,6 +1839,35 @@ def test_handle_repr(self):
18371839
% (cb_regex, re.escape(filename), lineno))
18381840
self.assertRegex(repr(h), regex)
18391841

1842+
def test_handle_source_traceback(self):
1843+
loop = asyncio.get_event_loop_policy().new_event_loop()
1844+
loop.set_debug(True)
1845+
self.set_event_loop(loop)
1846+
1847+
def check_source_traceback(h):
1848+
lineno = sys._getframe(1).f_lineno - 1
1849+
self.assertIsInstance(h._source_traceback, list)
1850+
self.assertEqual(h._source_traceback[-1][:3],
1851+
(__file__,
1852+
lineno,
1853+
'test_handle_source_traceback'))
1854+
1855+
# call_soon
1856+
h = loop.call_soon(noop)
1857+
check_source_traceback(h)
1858+
1859+
# call_soon_threadsafe
1860+
h = loop.call_soon_threadsafe(noop)
1861+
check_source_traceback(h)
1862+
1863+
# call_later
1864+
h = loop.call_later(0, noop)
1865+
check_source_traceback(h)
1866+
1867+
# call_at
1868+
h = loop.call_later(0, noop)
1869+
check_source_traceback(h)
1870+
18401871

18411872
class TimerTests(unittest.TestCase):
18421873

Lib/test/test_asyncio/test_futures.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import concurrent.futures
44
import re
5+
import sys
56
import threading
67
import unittest
8+
from test import support
79
from unittest import mock
810

911
import asyncio
@@ -284,6 +286,63 @@ def test_wrap_future_cancel2(self):
284286
self.assertEqual(f1.result(), 42)
285287
self.assertTrue(f2.cancelled())
286288

289+
def test_future_source_traceback(self):
290+
self.loop.set_debug(True)
291+
292+
future = asyncio.Future(loop=self.loop)
293+
lineno = sys._getframe().f_lineno - 1
294+
self.assertIsInstance(future._source_traceback, list)
295+
self.assertEqual(future._source_traceback[-1][:3],
296+
(__file__,
297+
lineno,
298+
'test_future_source_traceback'))
299+
300+
@mock.patch('asyncio.base_events.logger')
301+
def test_future_exception_never_retrieved(self, m_log):
302+
self.loop.set_debug(True)
303+
304+
def memroy_error():
305+
try:
306+
raise MemoryError()
307+
except BaseException as exc:
308+
return exc
309+
exc = memroy_error()
310+
311+
future = asyncio.Future(loop=self.loop)
312+
source_traceback = future._source_traceback
313+
future.set_exception(exc)
314+
future = None
315+
test_utils.run_briefly(self.loop)
316+
support.gc_collect()
317+
318+
if sys.version_info >= (3, 4):
319+
frame = source_traceback[-1]
320+
regex = (r'^Future exception was never retrieved\n'
321+
r'future: <Future finished exception=MemoryError\(\)>\n'
322+
r'source_traceback: Object created at \(most recent call last\):\n'
323+
r' File'
324+
r'.*\n'
325+
r' File "%s", line %s, in test_future_exception_never_retrieved\n'
326+
r' future = asyncio\.Future\(loop=self\.loop\)$'
327+
% (frame[0], frame[1]))
328+
exc_info = (type(exc), exc, exc.__traceback__)
329+
m_log.error.assert_called_once_with(mock.ANY, exc_info=exc_info)
330+
else:
331+
frame = source_traceback[-1]
332+
regex = (r'^Future/Task exception was never retrieved\n'
333+
r'Future/Task created at \(most recent call last\):\n'
334+
r' File'
335+
r'.*\n'
336+
r' File "%s", line %s, in test_future_exception_never_retrieved\n'
337+
r' future = asyncio\.Future\(loop=self\.loop\)\n'
338+
r'Traceback \(most recent call last\):\n'
339+
r'.*\n'
340+
r'MemoryError$'
341+
% (frame[0], frame[1]))
342+
m_log.error.assert_called_once_with(mock.ANY, exc_info=False)
343+
message = m_log.error.call_args[0][0]
344+
self.assertRegex(message, re.compile(regex, re.DOTALL))
345+
287346

288347
class FutureDoneCallbackTests(test_utils.TestCase):
289348

0 commit comments

Comments
 (0)