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

Skip to content

Commit e7efff5

Browse files
committed
(Merge 3.4) asyncio: Tulip issue 173: Enhance repr(Handle) and repr(Task)
repr(Handle) is shorter for function: "foo" instead of "<function foo at 0x...>". It now also includes the source of the callback, filename and line number where it was defined, if available. repr(Task) now also includes the current position in the code, filename and line number, if available. If the coroutine (generator) is done, the line number is omitted and "done" is added.
2 parents 2d8d9d5 + 307bccc commit e7efff5

5 files changed

Lines changed: 123 additions & 31 deletions

File tree

Lib/asyncio/events.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,29 @@
88
'get_child_watcher', 'set_child_watcher',
99
]
1010

11+
import functools
12+
import inspect
1113
import subprocess
1214
import threading
1315
import socket
16+
import sys
17+
18+
19+
_PY34 = sys.version_info >= (3, 4)
20+
21+
def _get_function_source(func):
22+
if _PY34:
23+
func = inspect.unwrap(func)
24+
elif hasattr(func, '__wrapped__'):
25+
func = func.__wrapped__
26+
if inspect.isfunction(func):
27+
code = func.__code__
28+
return (code.co_filename, code.co_firstlineno)
29+
if isinstance(func, functools.partial):
30+
return _get_function_source(func.func)
31+
if _PY34 and isinstance(func, functools.partialmethod):
32+
return _get_function_source(func.func)
33+
return None
1434

1535

1636
class Handle:
@@ -26,7 +46,15 @@ def __init__(self, callback, args, loop):
2646
self._cancelled = False
2747

2848
def __repr__(self):
29-
res = 'Handle({}, {})'.format(self._callback, self._args)
49+
cb_repr = getattr(self._callback, '__qualname__', None)
50+
if not cb_repr:
51+
cb_repr = str(self._callback)
52+
53+
source = _get_function_source(self._callback)
54+
if source:
55+
cb_repr += ' at %s:%s' % source
56+
57+
res = 'Handle({}, {})'.format(cb_repr, self._args)
3058
if self._cancelled:
3159
res += '<cancelled>'
3260
return res

Lib/asyncio/tasks.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,15 @@ def __repr__(self):
188188
i = res.find('<')
189189
if i < 0:
190190
i = len(res)
191-
res = res[:i] + '(<{}>)'.format(self._coro.__name__) + res[i:]
191+
text = self._coro.__name__
192+
coro = self._coro
193+
if inspect.isgenerator(coro):
194+
filename = coro.gi_code.co_filename
195+
if coro.gi_frame is not None:
196+
text += ' at %s:%s' % (filename, coro.gi_frame.f_lineno)
197+
else:
198+
text += ' done at %s' % filename
199+
res = res[:i] + '(<{}>)'.format(text) + res[i:]
192200
return res
193201

194202
def get_stack(self, *, limit=None):

Lib/asyncio/test_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,10 @@ class MockPattern(str):
372372
"""
373373
def __eq__(self, other):
374374
return bool(re.search(str(self), other, re.S))
375+
376+
377+
def get_function_source(func):
378+
source = events._get_function_source(func)
379+
if source is None:
380+
raise ValueError("unable to get the source of %r" % (func,))
381+
return source

Lib/test/test_asyncio/test_events.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io
66
import os
77
import platform
8+
import re
89
import signal
910
import socket
1011
try:
@@ -1737,62 +1738,97 @@ def create_event_loop(self):
17371738
return asyncio.SelectorEventLoop(selectors.SelectSelector())
17381739

17391740

1741+
def noop():
1742+
pass
1743+
1744+
17401745
class HandleTests(unittest.TestCase):
17411746

1747+
def setUp(self):
1748+
self.loop = None
1749+
17421750
def test_handle(self):
17431751
def callback(*args):
17441752
return args
17451753

17461754
args = ()
1747-
h = asyncio.Handle(callback, args, mock.Mock())
1755+
h = asyncio.Handle(callback, args, self.loop)
17481756
self.assertIs(h._callback, callback)
17491757
self.assertIs(h._args, args)
17501758
self.assertFalse(h._cancelled)
17511759

1752-
r = repr(h)
1753-
self.assertTrue(r.startswith(
1754-
'Handle('
1755-
'<function HandleTests.test_handle.<locals>.callback'))
1756-
self.assertTrue(r.endswith('())'))
1757-
17581760
h.cancel()
17591761
self.assertTrue(h._cancelled)
17601762

1761-
r = repr(h)
1762-
self.assertTrue(r.startswith(
1763-
'Handle('
1764-
'<function HandleTests.test_handle.<locals>.callback'))
1765-
self.assertTrue(r.endswith('())<cancelled>'), r)
1766-
17671763
def test_handle_from_handle(self):
17681764
def callback(*args):
17691765
return args
1770-
m_loop = object()
1771-
h1 = asyncio.Handle(callback, (), loop=m_loop)
1766+
h1 = asyncio.Handle(callback, (), loop=self.loop)
17721767
self.assertRaises(
1773-
AssertionError, asyncio.Handle, h1, (), m_loop)
1768+
AssertionError, asyncio.Handle, h1, (), self.loop)
17741769

17751770
def test_callback_with_exception(self):
17761771
def callback():
17771772
raise ValueError()
17781773

1779-
m_loop = mock.Mock()
1780-
m_loop.call_exception_handler = mock.Mock()
1774+
self.loop = mock.Mock()
1775+
self.loop.call_exception_handler = mock.Mock()
17811776

1782-
h = asyncio.Handle(callback, (), m_loop)
1777+
h = asyncio.Handle(callback, (), self.loop)
17831778
h._run()
17841779

1785-
m_loop.call_exception_handler.assert_called_with({
1780+
self.loop.call_exception_handler.assert_called_with({
17861781
'message': test_utils.MockPattern('Exception in callback.*'),
17871782
'exception': mock.ANY,
17881783
'handle': h
17891784
})
17901785

17911786
def test_handle_weakref(self):
17921787
wd = weakref.WeakValueDictionary()
1793-
h = asyncio.Handle(lambda: None, (), object())
1788+
h = asyncio.Handle(lambda: None, (), self.loop)
17941789
wd['h'] = h # Would fail without __weakref__ slot.
17951790

1791+
def test_repr(self):
1792+
# simple function
1793+
h = asyncio.Handle(noop, (), self.loop)
1794+
src = test_utils.get_function_source(noop)
1795+
self.assertEqual(repr(h),
1796+
'Handle(noop at %s:%s, ())' % src)
1797+
1798+
# cancelled handle
1799+
h.cancel()
1800+
self.assertEqual(repr(h),
1801+
'Handle(noop at %s:%s, ())<cancelled>' % src)
1802+
1803+
# decorated function
1804+
cb = asyncio.coroutine(noop)
1805+
h = asyncio.Handle(cb, (), self.loop)
1806+
self.assertEqual(repr(h),
1807+
'Handle(noop at %s:%s, ())' % src)
1808+
1809+
# partial function
1810+
cb = functools.partial(noop)
1811+
h = asyncio.Handle(cb, (), self.loop)
1812+
filename, lineno = src
1813+
regex = (r'^Handle\(functools.partial\('
1814+
r'<function noop .*>\) at %s:%s, '
1815+
r'\(\)\)$' % (re.escape(filename), lineno))
1816+
self.assertRegex(repr(h), regex)
1817+
1818+
# partial method
1819+
if sys.version_info >= (3, 4):
1820+
method = HandleTests.test_repr
1821+
cb = functools.partialmethod(method)
1822+
src = test_utils.get_function_source(method)
1823+
h = asyncio.Handle(cb, (), self.loop)
1824+
1825+
filename, lineno = src
1826+
regex = (r'^Handle\(functools.partialmethod\('
1827+
r'<function HandleTests.test_repr .*>, , \) at %s:%s, '
1828+
r'\(\)\)$' % (re.escape(filename), lineno))
1829+
self.assertRegex(repr(h), regex)
1830+
1831+
17961832

17971833
class TimerTests(unittest.TestCase):
17981834

Lib/test/test_asyncio/test_tasks.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,30 @@ def notmuch():
116116
yield from []
117117
return 'abc'
118118

119+
filename, lineno = test_utils.get_function_source(notmuch)
120+
src = "%s:%s" % (filename, lineno)
121+
119122
t = asyncio.Task(notmuch(), loop=self.loop)
120123
t.add_done_callback(Dummy())
121-
self.assertEqual(repr(t), 'Task(<notmuch>)<PENDING, [Dummy()]>')
124+
self.assertEqual(repr(t),
125+
'Task(<notmuch at %s>)<PENDING, [Dummy()]>' % src)
126+
122127
t.cancel() # Does not take immediate effect!
123-
self.assertEqual(repr(t), 'Task(<notmuch>)<CANCELLING, [Dummy()]>')
128+
self.assertEqual(repr(t),
129+
'Task(<notmuch at %s>)<CANCELLING, [Dummy()]>' % src)
124130
self.assertRaises(asyncio.CancelledError,
125131
self.loop.run_until_complete, t)
126-
self.assertEqual(repr(t), 'Task(<notmuch>)<CANCELLED>')
132+
self.assertEqual(repr(t),
133+
'Task(<notmuch done at %s>)<CANCELLED>' % filename)
134+
127135
t = asyncio.Task(notmuch(), loop=self.loop)
128136
self.loop.run_until_complete(t)
129-
self.assertEqual(repr(t), "Task(<notmuch>)<result='abc'>")
137+
self.assertEqual(repr(t),
138+
"Task(<notmuch done at %s>)<result='abc'>" % filename)
130139

131140
def test_task_repr_custom(self):
132141
@asyncio.coroutine
133-
def coro():
142+
def notmuch():
134143
pass
135144

136145
class T(asyncio.Future):
@@ -141,10 +150,14 @@ class MyTask(asyncio.Task, T):
141150
def __repr__(self):
142151
return super().__repr__()
143152

144-
gen = coro()
153+
gen = notmuch()
145154
t = MyTask(gen, loop=self.loop)
146-
self.assertEqual(repr(t), 'T[](<coro>)')
147-
gen.close()
155+
filename = gen.gi_code.co_filename
156+
lineno = gen.gi_frame.f_lineno
157+
# FIXME: check for the name "coro" instead of "notmuch" because
158+
# @asyncio.coroutine drops the name of the wrapped function:
159+
# http://bugs.python.org/issue21205
160+
self.assertEqual(repr(t), 'T[](<coro at %s:%s>)' % (filename, lineno))
148161

149162
def test_task_basics(self):
150163
@asyncio.coroutine

0 commit comments

Comments
 (0)