Commit 307bccc6 authored by Victor Stinner's avatar Victor Stinner

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.
parent f54432e2
...@@ -8,9 +8,29 @@ __all__ = ['AbstractEventLoopPolicy', ...@@ -8,9 +8,29 @@ __all__ = ['AbstractEventLoopPolicy',
'get_child_watcher', 'set_child_watcher', 'get_child_watcher', 'set_child_watcher',
] ]
import functools
import inspect
import subprocess import subprocess
import threading import threading
import socket import socket
import sys
_PY34 = sys.version_info >= (3, 4)
def _get_function_source(func):
if _PY34:
func = inspect.unwrap(func)
elif hasattr(func, '__wrapped__'):
func = func.__wrapped__
if inspect.isfunction(func):
code = func.__code__
return (code.co_filename, code.co_firstlineno)
if isinstance(func, functools.partial):
return _get_function_source(func.func)
if _PY34 and isinstance(func, functools.partialmethod):
return _get_function_source(func.func)
return None
class Handle: class Handle:
...@@ -26,7 +46,15 @@ class Handle: ...@@ -26,7 +46,15 @@ class Handle:
self._cancelled = False self._cancelled = False
def __repr__(self): def __repr__(self):
res = 'Handle({}, {})'.format(self._callback, self._args) cb_repr = getattr(self._callback, '__qualname__', None)
if not cb_repr:
cb_repr = str(self._callback)
source = _get_function_source(self._callback)
if source:
cb_repr += ' at %s:%s' % source
res = 'Handle({}, {})'.format(cb_repr, self._args)
if self._cancelled: if self._cancelled:
res += '<cancelled>' res += '<cancelled>'
return res return res
......
...@@ -188,7 +188,15 @@ class Task(futures.Future): ...@@ -188,7 +188,15 @@ class Task(futures.Future):
i = res.find('<') i = res.find('<')
if i < 0: if i < 0:
i = len(res) i = len(res)
res = res[:i] + '(<{}>)'.format(self._coro.__name__) + res[i:] text = self._coro.__name__
coro = self._coro
if inspect.isgenerator(coro):
filename = coro.gi_code.co_filename
if coro.gi_frame is not None:
text += ' at %s:%s' % (filename, coro.gi_frame.f_lineno)
else:
text += ' done at %s' % filename
res = res[:i] + '(<{}>)'.format(text) + res[i:]
return res return res
def get_stack(self, *, limit=None): def get_stack(self, *, limit=None):
......
...@@ -372,3 +372,10 @@ class MockPattern(str): ...@@ -372,3 +372,10 @@ class MockPattern(str):
""" """
def __eq__(self, other): def __eq__(self, other):
return bool(re.search(str(self), other, re.S)) return bool(re.search(str(self), other, re.S))
def get_function_source(func):
source = events._get_function_source(func)
if source is None:
raise ValueError("unable to get the source of %r" % (func,))
return source
...@@ -5,6 +5,7 @@ import gc ...@@ -5,6 +5,7 @@ import gc
import io import io
import os import os
import platform import platform
import re
import signal import signal
import socket import socket
try: try:
...@@ -1737,52 +1738,46 @@ else: ...@@ -1737,52 +1738,46 @@ else:
return asyncio.SelectorEventLoop(selectors.SelectSelector()) return asyncio.SelectorEventLoop(selectors.SelectSelector())
def noop():
pass
class HandleTests(unittest.TestCase): class HandleTests(unittest.TestCase):
def setUp(self):
self.loop = None
def test_handle(self): def test_handle(self):
def callback(*args): def callback(*args):
return args return args
args = () args = ()
h = asyncio.Handle(callback, args, mock.Mock()) h = asyncio.Handle(callback, args, self.loop)
self.assertIs(h._callback, callback) self.assertIs(h._callback, callback)
self.assertIs(h._args, args) self.assertIs(h._args, args)
self.assertFalse(h._cancelled) self.assertFalse(h._cancelled)
r = repr(h)
self.assertTrue(r.startswith(
'Handle('
'<function HandleTests.test_handle.<locals>.callback'))
self.assertTrue(r.endswith('())'))
h.cancel() h.cancel()
self.assertTrue(h._cancelled) self.assertTrue(h._cancelled)
r = repr(h)
self.assertTrue(r.startswith(
'Handle('
'<function HandleTests.test_handle.<locals>.callback'))
self.assertTrue(r.endswith('())<cancelled>'), r)
def test_handle_from_handle(self): def test_handle_from_handle(self):
def callback(*args): def callback(*args):
return args return args
m_loop = object() h1 = asyncio.Handle(callback, (), loop=self.loop)
h1 = asyncio.Handle(callback, (), loop=m_loop)
self.assertRaises( self.assertRaises(
AssertionError, asyncio.Handle, h1, (), m_loop) AssertionError, asyncio.Handle, h1, (), self.loop)
def test_callback_with_exception(self): def test_callback_with_exception(self):
def callback(): def callback():
raise ValueError() raise ValueError()
m_loop = mock.Mock() self.loop = mock.Mock()
m_loop.call_exception_handler = mock.Mock() self.loop.call_exception_handler = mock.Mock()
h = asyncio.Handle(callback, (), m_loop) h = asyncio.Handle(callback, (), self.loop)
h._run() h._run()
m_loop.call_exception_handler.assert_called_with({ self.loop.call_exception_handler.assert_called_with({
'message': test_utils.MockPattern('Exception in callback.*'), 'message': test_utils.MockPattern('Exception in callback.*'),
'exception': mock.ANY, 'exception': mock.ANY,
'handle': h 'handle': h
...@@ -1790,9 +1785,50 @@ class HandleTests(unittest.TestCase): ...@@ -1790,9 +1785,50 @@ class HandleTests(unittest.TestCase):
def test_handle_weakref(self): def test_handle_weakref(self):
wd = weakref.WeakValueDictionary() wd = weakref.WeakValueDictionary()
h = asyncio.Handle(lambda: None, (), object()) h = asyncio.Handle(lambda: None, (), self.loop)
wd['h'] = h # Would fail without __weakref__ slot. wd['h'] = h # Would fail without __weakref__ slot.
def test_repr(self):
# simple function
h = asyncio.Handle(noop, (), self.loop)
src = test_utils.get_function_source(noop)
self.assertEqual(repr(h),
'Handle(noop at %s:%s, ())' % src)
# cancelled handle
h.cancel()
self.assertEqual(repr(h),
'Handle(noop at %s:%s, ())<cancelled>' % src)
# decorated function
cb = asyncio.coroutine(noop)
h = asyncio.Handle(cb, (), self.loop)
self.assertEqual(repr(h),
'Handle(noop at %s:%s, ())' % src)
# partial function
cb = functools.partial(noop)
h = asyncio.Handle(cb, (), self.loop)
filename, lineno = src
regex = (r'^Handle\(functools.partial\('
r'<function noop .*>\) at %s:%s, '
r'\(\)\)$' % (re.escape(filename), lineno))
self.assertRegex(repr(h), regex)
# partial method
if sys.version_info >= (3, 4):
method = HandleTests.test_repr
cb = functools.partialmethod(method)
src = test_utils.get_function_source(method)
h = asyncio.Handle(cb, (), self.loop)
filename, lineno = src
regex = (r'^Handle\(functools.partialmethod\('
r'<function HandleTests.test_repr .*>, , \) at %s:%s, '
r'\(\)\)$' % (re.escape(filename), lineno))
self.assertRegex(repr(h), regex)
class TimerTests(unittest.TestCase): class TimerTests(unittest.TestCase):
......
...@@ -116,21 +116,30 @@ class TaskTests(unittest.TestCase): ...@@ -116,21 +116,30 @@ class TaskTests(unittest.TestCase):
yield from [] yield from []
return 'abc' return 'abc'
filename, lineno = test_utils.get_function_source(notmuch)
src = "%s:%s" % (filename, lineno)
t = asyncio.Task(notmuch(), loop=self.loop) t = asyncio.Task(notmuch(), loop=self.loop)
t.add_done_callback(Dummy()) t.add_done_callback(Dummy())
self.assertEqual(repr(t), 'Task(<notmuch>)<PENDING, [Dummy()]>') self.assertEqual(repr(t),
'Task(<notmuch at %s>)<PENDING, [Dummy()]>' % src)
t.cancel() # Does not take immediate effect! t.cancel() # Does not take immediate effect!
self.assertEqual(repr(t), 'Task(<notmuch>)<CANCELLING, [Dummy()]>') self.assertEqual(repr(t),
'Task(<notmuch at %s>)<CANCELLING, [Dummy()]>' % src)
self.assertRaises(asyncio.CancelledError, self.assertRaises(asyncio.CancelledError,
self.loop.run_until_complete, t) self.loop.run_until_complete, t)
self.assertEqual(repr(t), 'Task(<notmuch>)<CANCELLED>') self.assertEqual(repr(t),
'Task(<notmuch done at %s>)<CANCELLED>' % filename)
t = asyncio.Task(notmuch(), loop=self.loop) t = asyncio.Task(notmuch(), loop=self.loop)
self.loop.run_until_complete(t) self.loop.run_until_complete(t)
self.assertEqual(repr(t), "Task(<notmuch>)<result='abc'>") self.assertEqual(repr(t),
"Task(<notmuch done at %s>)<result='abc'>" % filename)
def test_task_repr_custom(self): def test_task_repr_custom(self):
@asyncio.coroutine @asyncio.coroutine
def coro(): def notmuch():
pass pass
class T(asyncio.Future): class T(asyncio.Future):
...@@ -141,10 +150,14 @@ class TaskTests(unittest.TestCase): ...@@ -141,10 +150,14 @@ class TaskTests(unittest.TestCase):
def __repr__(self): def __repr__(self):
return super().__repr__() return super().__repr__()
gen = coro() gen = notmuch()
t = MyTask(gen, loop=self.loop) t = MyTask(gen, loop=self.loop)
self.assertEqual(repr(t), 'T[](<coro>)') filename = gen.gi_code.co_filename
gen.close() lineno = gen.gi_frame.f_lineno
# FIXME: check for the name "coro" instead of "notmuch" because
# @asyncio.coroutine drops the name of the wrapped function:
# http://bugs.python.org/issue21205
self.assertEqual(repr(t), 'T[](<coro at %s:%s>)' % (filename, lineno))
def test_task_basics(self): def test_task_basics(self):
@asyncio.coroutine @asyncio.coroutine
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment