mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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.
This commit is contained in:
parent
f54432e2a1
commit
307bccc6ff
5 changed files with 123 additions and 31 deletions
|
@ -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:
|
||||||
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):
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
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:
|
||||||
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):
|
||||||
|
|
||||||
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):
|
||||||
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):
|
||||||
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue