Skip to content

Commit 975735f

Browse files
committed
asyncio, Tulip issue 177: Rewite repr() of Future, Task, Handle and TimerHandle
- Uniformize repr() output to format "<Class ...>" - On Python 3.5+, repr(Task) uses the qualified name instead of the short name of the coroutine
1 parent 65c623d commit 975735f

7 files changed

Lines changed: 231 additions & 128 deletions

File tree

Lib/asyncio/events.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
_PY34 = sys.version_info >= (3, 4)
2020

21+
2122
def _get_function_source(func):
2223
if _PY34:
2324
func = inspect.unwrap(func)
@@ -33,6 +34,35 @@ def _get_function_source(func):
3334
return None
3435

3536

37+
def _format_args(args):
38+
# function formatting ('hello',) as ('hello')
39+
args_repr = repr(args)
40+
if len(args) == 1 and args_repr.endswith(',)'):
41+
args_repr = args_repr[:-2] + ')'
42+
return args_repr
43+
44+
45+
def _format_callback(func, args, suffix=''):
46+
if isinstance(func, functools.partial):
47+
if args is not None:
48+
suffix = _format_args(args) + suffix
49+
return _format_callback(func.func, func.args, suffix)
50+
51+
func_repr = getattr(func, '__qualname__', None)
52+
if not func_repr:
53+
func_repr = repr(func)
54+
55+
if args is not None:
56+
func_repr += _format_args(args)
57+
if suffix:
58+
func_repr += suffix
59+
60+
source = _get_function_source(func)
61+
if source:
62+
func_repr += ' at %s:%s' % source
63+
return func_repr
64+
65+
3666
class Handle:
3767
"""Object returned by callback registration methods."""
3868

@@ -46,18 +76,11 @@ def __init__(self, callback, args, loop):
4676
self._cancelled = False
4777

4878
def __repr__(self):
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)
79+
info = []
5880
if self._cancelled:
59-
res += '<cancelled>'
60-
return res
81+
info.append('cancelled')
82+
info.append(_format_callback(self._callback, self._args))
83+
return '<%s %s>' % (self.__class__.__name__, ' '.join(info))
6184

6285
def cancel(self):
6386
self._cancelled = True
@@ -88,13 +111,12 @@ def __init__(self, when, callback, args, loop):
88111
self._when = when
89112

90113
def __repr__(self):
91-
res = 'TimerHandle({}, {}, {})'.format(self._when,
92-
self._callback,
93-
self._args)
114+
info = []
94115
if self._cancelled:
95-
res += '<cancelled>'
96-
97-
return res
116+
info.append('cancelled')
117+
info.append('when=%s' % self._when)
118+
info.append(_format_callback(self._callback, self._args))
119+
return '<%s %s>' % (self.__class__.__name__, ' '.join(info))
98120

99121
def __hash__(self):
100122
return hash(self._when)

Lib/asyncio/futures.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,24 +150,40 @@ def __init__(self, *, loop=None):
150150
self._loop = loop
151151
self._callbacks = []
152152

153+
def _format_callbacks(self):
154+
cb = self._callbacks
155+
size = len(cb)
156+
if not size:
157+
cb = ''
158+
159+
def format_cb(callback):
160+
return events._format_callback(callback, ())
161+
162+
if size == 1:
163+
cb = format_cb(cb[0])
164+
elif size == 2:
165+
cb = '{}, {}'.format(format_cb(cb[0]), format_cb(cb[1]))
166+
elif size > 2:
167+
cb = '{}, <{} more>, {}'.format(format_cb(cb[0]),
168+
size-2,
169+
format_cb(cb[-1]))
170+
return 'cb=[%s]' % cb
171+
172+
def _format_result(self):
173+
if self._state != _FINISHED:
174+
return None
175+
elif self._exception is not None:
176+
return 'exception={!r}'.format(self._exception)
177+
else:
178+
return 'result={!r}'.format(self._result)
179+
153180
def __repr__(self):
154-
res = self.__class__.__name__
181+
info = [self._state.lower()]
155182
if self._state == _FINISHED:
156-
if self._exception is not None:
157-
res += '<exception={!r}>'.format(self._exception)
158-
else:
159-
res += '<result={!r}>'.format(self._result)
160-
elif self._callbacks:
161-
size = len(self._callbacks)
162-
if size > 2:
163-
res += '<{}, [{}, <{} more>, {}]>'.format(
164-
self._state, self._callbacks[0],
165-
size-2, self._callbacks[-1])
166-
else:
167-
res += '<{}, {}>'.format(self._state, self._callbacks)
168-
else:
169-
res += '<{}>'.format(self._state)
170-
return res
183+
info.append(self._format_result())
184+
if self._callbacks:
185+
info.append(self._format_callbacks())
186+
return '<%s %s>' % (self.__class__.__name__, ' '.join(info))
171187

172188
# On Python 3.3 or older, objects with a destructor part of a reference
173189
# cycle are never destroyed. It's not more the case on Python 3.4 thanks to

Lib/asyncio/tasks.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ def iscoroutine(obj):
132132
return isinstance(obj, CoroWrapper) or inspect.isgenerator(obj)
133133

134134

135+
def _format_coroutine(coro):
136+
assert iscoroutine(coro)
137+
if _PY35:
138+
coro_name = coro.__qualname__
139+
else:
140+
coro_name = coro.__name__
141+
142+
filename = coro.gi_code.co_filename
143+
if coro.gi_frame is not None:
144+
lineno = coro.gi_frame.f_lineno
145+
return '%s() at %s:%s' % (coro_name, filename, lineno)
146+
else:
147+
lineno = coro.gi_code.co_firstlineno
148+
return '%s() done at %s:%s' % (coro_name, filename, lineno)
149+
150+
135151
class Task(futures.Future):
136152
"""A coroutine wrapped in a Future."""
137153

@@ -195,26 +211,21 @@ def __del__(self):
195211
futures.Future.__del__(self)
196212

197213
def __repr__(self):
198-
res = super().__repr__()
199-
if (self._must_cancel and
200-
self._state == futures._PENDING and
201-
'<PENDING' in res):
202-
res = res.replace('<PENDING', '<CANCELLING', 1)
203-
i = res.find('<')
204-
if i < 0:
205-
i = len(res)
206-
text = self._coro.__name__
207-
coro = self._coro
208-
if iscoroutine(coro):
209-
filename = coro.gi_code.co_filename
210-
if coro.gi_frame is not None:
211-
lineno = coro.gi_frame.f_lineno
212-
text += ' at %s:%s' % (filename, lineno)
213-
else:
214-
lineno = coro.gi_code.co_firstlineno
215-
text += ' done at %s:%s' % (filename, lineno)
216-
res = res[:i] + '(<{}>)'.format(text) + res[i:]
217-
return res
214+
info = []
215+
if self._must_cancel:
216+
info.append('cancelling')
217+
else:
218+
info.append(self._state.lower())
219+
220+
info.append(_format_coroutine(self._coro))
221+
222+
if self._state == futures._FINISHED:
223+
info.append(self._format_result())
224+
225+
if self._callbacks:
226+
info.append(self._format_callbacks())
227+
228+
return '<%s %s>' % (self.__class__.__name__, ' '.join(info))
218229

219230
def get_stack(self, *, limit=None):
220231
"""Return the list of stack frames for this task's coroutine.

Lib/test/test_asyncio/test_base_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,14 +1016,14 @@ def stop_loop_coro(loop):
10161016
self.loop.run_forever()
10171017
fmt, *args = m_logger.warning.call_args[0]
10181018
self.assertRegex(fmt % tuple(args),
1019-
"^Executing Handle.*stop_loop_cb.* took .* seconds$")
1019+
"^Executing <Handle.*stop_loop_cb.*> took .* seconds$")
10201020

10211021
# slow task
10221022
asyncio.async(stop_loop_coro(self.loop), loop=self.loop)
10231023
self.loop.run_forever()
10241024
fmt, *args = m_logger.warning.call_args[0]
10251025
self.assertRegex(fmt % tuple(args),
1026-
"^Executing Task.*stop_loop_coro.* took .* seconds$")
1026+
"^Executing <Task.*stop_loop_coro.*> took .* seconds$")
10271027

10281028

10291029
if __name__ == '__main__':

Lib/test/test_asyncio/test_events.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,7 +1747,7 @@ def create_event_loop(self):
17471747
return asyncio.SelectorEventLoop(selectors.SelectSelector())
17481748

17491749

1750-
def noop():
1750+
def noop(*args):
17511751
pass
17521752

17531753

@@ -1797,50 +1797,52 @@ def test_handle_weakref(self):
17971797
h = asyncio.Handle(lambda: None, (), self.loop)
17981798
wd['h'] = h # Would fail without __weakref__ slot.
17991799

1800-
def test_repr(self):
1800+
def test_handle_repr(self):
18011801
# simple function
18021802
h = asyncio.Handle(noop, (), self.loop)
18031803
src = test_utils.get_function_source(noop)
18041804
self.assertEqual(repr(h),
1805-
'Handle(noop at %s:%s, ())' % src)
1805+
'<Handle noop() at %s:%s>' % src)
18061806

18071807
# cancelled handle
18081808
h.cancel()
18091809
self.assertEqual(repr(h),
1810-
'Handle(noop at %s:%s, ())<cancelled>' % src)
1810+
'<Handle cancelled noop() at %s:%s>' % src)
18111811

18121812
# decorated function
18131813
cb = asyncio.coroutine(noop)
18141814
h = asyncio.Handle(cb, (), self.loop)
18151815
self.assertEqual(repr(h),
1816-
'Handle(noop at %s:%s, ())' % src)
1816+
'<Handle noop() at %s:%s>' % src)
18171817

18181818
# partial function
1819-
cb = functools.partial(noop)
1820-
h = asyncio.Handle(cb, (), self.loop)
1819+
cb = functools.partial(noop, 1, 2)
1820+
h = asyncio.Handle(cb, (3,), self.loop)
18211821
filename, lineno = src
1822-
regex = (r'^Handle\(functools.partial\('
1823-
r'<function noop .*>\) at %s:%s, '
1824-
r'\(\)\)$' % (re.escape(filename), lineno))
1822+
regex = (r'^<Handle noop\(1, 2\)\(3\) at %s:%s>$'
1823+
% (re.escape(filename), lineno))
18251824
self.assertRegex(repr(h), regex)
18261825

18271826
# partial method
18281827
if sys.version_info >= (3, 4):
1829-
method = HandleTests.test_repr
1828+
method = HandleTests.test_handle_repr
18301829
cb = functools.partialmethod(method)
18311830
src = test_utils.get_function_source(method)
18321831
h = asyncio.Handle(cb, (), self.loop)
18331832

18341833
filename, lineno = src
1835-
regex = (r'^Handle\(functools.partialmethod\('
1836-
r'<function HandleTests.test_repr .*>, , \) at %s:%s, '
1837-
r'\(\)\)$' % (re.escape(filename), lineno))
1834+
cb_regex = r'<function HandleTests.test_handle_repr .*>'
1835+
cb_regex = (r'functools.partialmethod\(%s, , \)\(\)' % cb_regex)
1836+
regex = (r'^<Handle %s at %s:%s>$'
1837+
% (cb_regex, re.escape(filename), lineno))
18381838
self.assertRegex(repr(h), regex)
18391839

18401840

1841-
18421841
class TimerTests(unittest.TestCase):
18431842

1843+
def setUp(self):
1844+
self.loop = mock.Mock()
1845+
18441846
def test_hash(self):
18451847
when = time.monotonic()
18461848
h = asyncio.TimerHandle(when, lambda: False, (),
@@ -1858,29 +1860,37 @@ def callback(*args):
18581860
self.assertIs(h._args, args)
18591861
self.assertFalse(h._cancelled)
18601862

1861-
r = repr(h)
1862-
self.assertTrue(r.endswith('())'))
1863-
1863+
# cancel
18641864
h.cancel()
18651865
self.assertTrue(h._cancelled)
18661866

1867-
r = repr(h)
1868-
self.assertTrue(r.endswith('())<cancelled>'), r)
18691867

1868+
# when cannot be None
18701869
self.assertRaises(AssertionError,
18711870
asyncio.TimerHandle, None, callback, args,
1872-
mock.Mock())
1871+
self.loop)
18731872

1874-
def test_timer_comparison(self):
1875-
loop = mock.Mock()
1873+
def test_timer_repr(self):
1874+
# simple function
1875+
h = asyncio.TimerHandle(123, noop, (), self.loop)
1876+
src = test_utils.get_function_source(noop)
1877+
self.assertEqual(repr(h),
1878+
'<TimerHandle when=123 noop() at %s:%s>' % src)
18761879

1880+
# cancelled handle
1881+
h.cancel()
1882+
self.assertEqual(repr(h),
1883+
'<TimerHandle cancelled when=123 noop() at %s:%s>'
1884+
% src)
1885+
1886+
def test_timer_comparison(self):
18771887
def callback(*args):
18781888
return args
18791889

18801890
when = time.monotonic()
18811891

1882-
h1 = asyncio.TimerHandle(when, callback, (), loop)
1883-
h2 = asyncio.TimerHandle(when, callback, (), loop)
1892+
h1 = asyncio.TimerHandle(when, callback, (), self.loop)
1893+
h2 = asyncio.TimerHandle(when, callback, (), self.loop)
18841894
# TODO: Use assertLess etc.
18851895
self.assertFalse(h1 < h2)
18861896
self.assertFalse(h2 < h1)
@@ -1896,8 +1906,8 @@ def callback(*args):
18961906
h2.cancel()
18971907
self.assertFalse(h1 == h2)
18981908

1899-
h1 = asyncio.TimerHandle(when, callback, (), loop)
1900-
h2 = asyncio.TimerHandle(when + 10.0, callback, (), loop)
1909+
h1 = asyncio.TimerHandle(when, callback, (), self.loop)
1910+
h2 = asyncio.TimerHandle(when + 10.0, callback, (), self.loop)
19011911
self.assertTrue(h1 < h2)
19021912
self.assertFalse(h2 < h1)
19031913
self.assertTrue(h1 <= h2)
@@ -1909,7 +1919,7 @@ def callback(*args):
19091919
self.assertFalse(h1 == h2)
19101920
self.assertTrue(h1 != h2)
19111921

1912-
h3 = asyncio.Handle(callback, (), loop)
1922+
h3 = asyncio.Handle(callback, (), self.loop)
19131923
self.assertIs(NotImplemented, h1.__eq__(h3))
19141924
self.assertIs(NotImplemented, h1.__ne__(h3))
19151925

0 commit comments

Comments
 (0)