From 307bccc6ff6670c58f4c20421a29071ff710e6a3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Jun 2014 18:39:26 +0200 Subject: asyncio: Tulip issue 173: Enhance repr(Handle) and repr(Task) repr(Handle) is shorter for function: "foo" instead of "". 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. --- 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 index 4a9a9a3..de161df 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -8,9 +8,29 @@ __all__ = ['AbstractEventLoopPolicy', '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 @@ class Handle: 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 += '' return res diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 8b8fb82..e6fd3d3 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -188,7 +188,15 @@ class Task(futures.Future): 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 index 9c3656a..1062bae 100644 --- a/Lib/asyncio/test_utils.py +++ b/Lib/asyncio/test_utils.py @@ -372,3 +372,10 @@ class MockPattern(str): """ 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 index e19d991..2262a75 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -5,6 +5,7 @@ import gc import io import os import platform +import re import signal import socket try: @@ -1737,52 +1738,46 @@ else: 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(' - '.callback')) - self.assertTrue(r.endswith('())')) - h.cancel() self.assertTrue(h._cancelled) - r = repr(h) - self.assertTrue(r.startswith( - 'Handle(' - '.callback')) - self.assertTrue(r.endswith('())'), 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 @@ class HandleTests(unittest.TestCase): 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, ())' % 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'\) 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', , \) 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 index 45a0dc1..92eb9da 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -116,21 +116,30 @@ class TaskTests(unittest.TestCase): 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()') + self.assertEqual(repr(t), + 'Task()' % src) + t.cancel() # Does not take immediate effect! - self.assertEqual(repr(t), 'Task()') + self.assertEqual(repr(t), + 'Task()' % src) self.assertRaises(asyncio.CancelledError, self.loop.run_until_complete, t) - self.assertEqual(repr(t), 'Task()') + self.assertEqual(repr(t), + 'Task()' % filename) + t = asyncio.Task(notmuch(), loop=self.loop) self.loop.run_until_complete(t) - self.assertEqual(repr(t), "Task()") + self.assertEqual(repr(t), + "Task()" % filename) def test_task_repr_custom(self): @asyncio.coroutine - def coro(): + def notmuch(): pass class T(asyncio.Future): @@ -141,10 +150,14 @@ class TaskTests(unittest.TestCase): def __repr__(self): return super().__repr__() - gen = coro() + gen = notmuch() t = MyTask(gen, loop=self.loop) - self.assertEqual(repr(t), 'T[]()') - 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[]()' % (filename, lineno)) def test_task_basics(self): @asyncio.coroutine -- cgit v0.12