[Python-checkins] cpython (merge 3.4 -> default): (Merge 3.4) asyncio: Tulip issue 173: Enhance repr(Handle) and repr(Task)
victor.stinner
python-checkins at python.org
Thu Jun 12 18:43:12 CEST 2014
http://hg.python.org/cpython/rev/04cc9c32f3e3
changeset: 91147:04cc9c32f3e3
parent: 91145:9aba5d75ce94
parent: 91146:e0395363f22f
user: Victor Stinner <victor.stinner at gmail.com>
date: Thu Jun 12 18:39:42 2014 +0200
summary:
(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.
files:
Lib/asyncio/events.py | 30 +++++-
Lib/asyncio/tasks.py | 10 +-
Lib/asyncio/test_utils.py | 7 +
Lib/test/test_asyncio/test_events.py | 78 +++++++++++----
Lib/test/test_asyncio/test_tasks.py | 29 ++++-
5 files changed, 123 insertions(+), 31 deletions(-)
diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py
--- a/Lib/asyncio/events.py
+++ b/Lib/asyncio/events.py
@@ -8,9 +8,29 @@
'get_child_watcher', 'set_child_watcher',
]
+import functools
+import inspect
import subprocess
import threading
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:
@@ -26,7 +46,15 @@
self._cancelled = False
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:
res += '<cancelled>'
return res
diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -188,7 +188,15 @@
i = res.find('<')
if i < 0:
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
def get_stack(self, *, limit=None):
diff --git a/Lib/asyncio/test_utils.py b/Lib/asyncio/test_utils.py
--- a/Lib/asyncio/test_utils.py
+++ b/Lib/asyncio/test_utils.py
@@ -372,3 +372,10 @@
"""
def __eq__(self, other):
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
diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py
--- a/Lib/test/test_asyncio/test_events.py
+++ b/Lib/test/test_asyncio/test_events.py
@@ -5,6 +5,7 @@
import io
import os
import platform
+import re
import signal
import socket
try:
@@ -1737,52 +1738,46 @@
return asyncio.SelectorEventLoop(selectors.SelectSelector())
+def noop():
+ pass
+
+
class HandleTests(unittest.TestCase):
+ def setUp(self):
+ self.loop = None
+
def test_handle(self):
def callback(*args):
return args
args = ()
- h = asyncio.Handle(callback, args, mock.Mock())
+ h = asyncio.Handle(callback, args, self.loop)
self.assertIs(h._callback, callback)
self.assertIs(h._args, args)
self.assertFalse(h._cancelled)
- r = repr(h)
- self.assertTrue(r.startswith(
- 'Handle('
- '<function HandleTests.test_handle.<locals>.callback'))
- self.assertTrue(r.endswith('())'))
-
h.cancel()
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 callback(*args):
return args
- m_loop = object()
- h1 = asyncio.Handle(callback, (), loop=m_loop)
+ h1 = asyncio.Handle(callback, (), loop=self.loop)
self.assertRaises(
- AssertionError, asyncio.Handle, h1, (), m_loop)
+ AssertionError, asyncio.Handle, h1, (), self.loop)
def test_callback_with_exception(self):
def callback():
raise ValueError()
- m_loop = mock.Mock()
- m_loop.call_exception_handler = mock.Mock()
+ self.loop = mock.Mock()
+ self.loop.call_exception_handler = mock.Mock()
- h = asyncio.Handle(callback, (), m_loop)
+ h = asyncio.Handle(callback, (), self.loop)
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.*'),
'exception': mock.ANY,
'handle': h
@@ -1790,9 +1785,50 @@
def test_handle_weakref(self):
wd = weakref.WeakValueDictionary()
- h = asyncio.Handle(lambda: None, (), object())
+ h = asyncio.Handle(lambda: None, (), self.loop)
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):
diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -116,21 +116,30 @@
yield from []
return 'abc'
+ filename, lineno = test_utils.get_function_source(notmuch)
+ src = "%s:%s" % (filename, lineno)
+
t = asyncio.Task(notmuch(), loop=self.loop)
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!
- self.assertEqual(repr(t), 'Task(<notmuch>)<CANCELLING, [Dummy()]>')
+ self.assertEqual(repr(t),
+ 'Task(<notmuch at %s>)<CANCELLING, [Dummy()]>' % src)
self.assertRaises(asyncio.CancelledError,
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)
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):
@asyncio.coroutine
- def coro():
+ def notmuch():
pass
class T(asyncio.Future):
@@ -141,10 +150,14 @@
def __repr__(self):
return super().__repr__()
- gen = coro()
+ gen = notmuch()
t = MyTask(gen, loop=self.loop)
- self.assertEqual(repr(t), 'T[](<coro>)')
- gen.close()
+ filename = gen.gi_code.co_filename
+ 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):
@asyncio.coroutine
--
Repository URL: http://hg.python.org/cpython
More information about the Python-checkins
mailing list