]> granicus.if.org Git - python/commitdiff
asyncio: Tulip issue 173: Enhance repr(Handle) and repr(Task)
authorVictor Stinner <victor.stinner@gmail.com>
Thu, 12 Jun 2014 16:39:26 +0000 (18:39 +0200)
committerVictor Stinner <victor.stinner@gmail.com>
Thu, 12 Jun 2014 16:39:26 +0000 (18:39 +0200)
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.

Lib/asyncio/events.py
Lib/asyncio/tasks.py
Lib/asyncio/test_utils.py
Lib/test/test_asyncio/test_events.py
Lib/test/test_asyncio/test_tasks.py

index 4a9a9a388571e40fddc59cb2399de8cc8afa132a..de161df65f4f47d7e79e7e7468581f4a5d62ac84 100644 (file)
@@ -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 += '<cancelled>'
         return res
index 8b8fb82ed2dfa070b0578695f78b37b635cd354a..e6fd3d380bebe2e8a7b316a5e9e61084d20755f5 100644 (file)
@@ -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):
index 9c3656ac2bea23a626995c5838be7be26052ff08..1062bae132234a69ae6fd7506534b838c461fec5 100644 (file)
@@ -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
index e19d991fc44fa819e1f7b7e8a484daf367857b21..2262a75226b461935fc213273086e46354cee520 100644 (file)
@@ -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('
-            '<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 @@ 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, ())<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):
 
index 45a0dc1d210d42f69005dd27254ea261fabc7d8b..92eb9daefbd9b210f3b8c1ec8917cfaef5d504b0 100644 (file)
@@ -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(<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 @@ 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[](<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