]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Introduce IOLoop.call_later and call_at.
authorBen Darnell <ben@bendarnell.com>
Wed, 18 Jun 2014 14:29:28 +0000 (10:29 -0400)
committerBen Darnell <ben@bendarnell.com>
Wed, 18 Jun 2014 14:29:28 +0000 (10:29 -0400)
call_later is a less-verbose alternative to add_timeout with a
timedelta; call_at exists for symmetry.  Both are named after
methods on the asyncio event loop, although there are small
variations (we support both args and kwargs while asyncio only supports
args; we use remove_timeout(handle) instead of handle.cancel()).

Closes #1049.

docs/ioloop.rst
tornado/ioloop.py
tornado/platform/asyncio.py
tornado/platform/twisted.py
tornado/test/ioloop_test.py
tornado/util.py

index bd364cc4d79d9e1533ca5206c632a0322310a06a..b04fb7ce4ec43d101a769952b5cd04ca7b9aff4a 100644 (file)
@@ -36,6 +36,8 @@
    .. automethod:: IOLoop.add_callback_from_signal
    .. automethod:: IOLoop.add_future
    .. automethod:: IOLoop.add_timeout
+   .. automethod:: IOLoop.call_at
+   .. automethod:: IOLoop.call_later
    .. automethod:: IOLoop.remove_timeout
    .. automethod:: IOLoop.time
    .. autoclass:: PeriodicCallback
index 3477684cbf3369dd03f53473269a66a05870a3be..da9b7dbda599f139e9681150500ea56234d93cf8 100644 (file)
@@ -45,8 +45,7 @@ import traceback
 from tornado.concurrent import TracebackFuture, is_future
 from tornado.log import app_log, gen_log
 from tornado import stack_context
-from tornado.util import Configurable
-from tornado.util import errno_from_exception
+from tornado.util import Configurable, errno_from_exception, timedelta_to_seconds
 
 try:
     import signal
@@ -433,7 +432,7 @@ class IOLoop(Configurable):
         """
         return time.time()
 
-    def add_timeout(self, deadline, callback):
+    def add_timeout(self, deadline, callback, *args, **kwargs):
         """Runs the ``callback`` at the time ``deadline`` from the I/O loop.
 
         Returns an opaque handle that may be passed to
@@ -442,13 +441,59 @@ class IOLoop(Configurable):
         ``deadline`` may be a number denoting a time (on the same
         scale as `IOLoop.time`, normally `time.time`), or a
         `datetime.timedelta` object for a deadline relative to the
-        current time.
+        current time.  Since Tornado 4.0, `call_later` is a more
+        convenient alternative for the relative case since it does not
+        require a timedelta object.
 
         Note that it is not safe to call `add_timeout` from other threads.
         Instead, you must use `add_callback` to transfer control to the
         `IOLoop`'s thread, and then call `add_timeout` from there.
+
+        Subclasses of IOLoop must implement either `add_timeout` or
+        `call_at`; the default implementations of each will call
+        the other.  `call_at` is usually easier to implement, but
+        subclasses that wish to maintain compatibility with Tornado
+        versions prior to 4.0 must use `add_timeout` instead.
+
+        .. versionchanged:: 4.0
+           Now passes through ``*args`` and ``**kwargs`` to the callback.
         """
-        raise NotImplementedError()
+        if isinstance(deadline, numbers.Real):
+            return self.call_at(deadline, callback, *args, **kwargs)
+        elif isinstance(deadline, datetime.timedelta):
+            return self.call_at(self.time() + timedelta_to_seconds(deadline),
+                                callback, *args, **kwargs)
+        else:
+            raise TypeError("Unsupported deadline %r" % deadline)
+
+    def call_later(self, delay, callback, *args, **kwargs):
+        """Runs the ``callback`` after ``delay`` seconds have passed.
+
+        Returns an opaque handle that may be passed to `remove_timeout`
+        to cancel.  Note that unlike the `asyncio` method of the same
+        name, the returned object does not have a ``cancel()`` method.
+
+        See `add_timeout` for comments on thread-safety and subclassing.
+
+        .. versionadded:: 4.0
+        """
+        self.call_at(self.time() + delay, callback, *args, **kwargs)
+
+    def call_at(self, when, callback, *args, **kwargs):
+        """Runs the ``callback`` at the absolute time designated by ``when``.
+
+        ``when`` must be a number using the same reference point as
+        `IOLoop.time`.
+
+        Returns an opaque handle that may be passed to `remove_timeout`
+        to cancel.  Note that unlike the `asyncio` method of the same
+        name, the returned object does not have a ``cancel()`` method.
+
+        See `add_timeout` for comments on thread-safety and subclassing.
+
+        .. versionadded:: 4.0
+        """
+        self.add_timeout(when, callback, *args, **kwargs)
 
     def remove_timeout(self, timeout):
         """Cancels a pending timeout.
@@ -606,7 +651,7 @@ class PollIOLoop(IOLoop):
         self._thread_ident = None
         self._blocking_signal_threshold = None
         self._timeout_counter = itertools.count()
-        
+
         # Create a pipe that we send bogus data to when we want to wake
         # the I/O loop when it is idle
         self._waker = Waker()
@@ -813,8 +858,11 @@ class PollIOLoop(IOLoop):
     def time(self):
         return self.time_func()
 
-    def add_timeout(self, deadline, callback):
-        timeout = _Timeout(deadline, stack_context.wrap(callback), self)
+    def call_at(self, deadline, callback, *args, **kwargs):
+        timeout = _Timeout(
+            deadline,
+            functools.partial(stack_context.wrap(callback), *args, **kwargs),
+            self)
         heapq.heappush(self._timeouts, timeout)
         return timeout
 
@@ -869,24 +917,12 @@ class _Timeout(object):
     __slots__ = ['deadline', 'callback', 'tiebreaker']
 
     def __init__(self, deadline, callback, io_loop):
-        if isinstance(deadline, numbers.Real):
-            self.deadline = deadline
-        elif isinstance(deadline, datetime.timedelta):
-            now = io_loop.time()
-            try:
-                self.deadline = now + deadline.total_seconds()
-            except AttributeError:  # py2.6
-                self.deadline = now + _Timeout.timedelta_to_seconds(deadline)
-        else:
+        if not isinstance(deadline, numbers.Real):
             raise TypeError("Unsupported deadline %r" % deadline)
+        self.deadline = deadline
         self.callback = callback
         self.tiebreaker = next(io_loop._timeout_counter)
 
-    @staticmethod
-    def timedelta_to_seconds(td):
-        """Equivalent to td.total_seconds() (introduced in python 2.7)."""
-        return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
-
     # Comparison methods to sort by deadline, with object id as a tiebreaker
     # to guarantee a consistent ordering.  The heapq module uses __le__
     # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
index 6518dea55900d7cec82407861376df5b004908be..b40f0141a37fc0f0872f8da04900f87ea24025f7 100644 (file)
@@ -13,9 +13,9 @@ from __future__ import absolute_import, division, print_function, with_statement
 import datetime
 import functools
 
-# _Timeout is used for its timedelta_to_seconds method for py26 compatibility.
-from tornado.ioloop import IOLoop, _Timeout
+from tornado.ioloop import IOLoop
 from tornado import stack_context
+from tornado.util import timedelta_to_seconds
 
 try:
     # Import the real asyncio module for py33+ first.  Older versions of the
@@ -109,15 +109,13 @@ class BaseAsyncIOLoop(IOLoop):
     def stop(self):
         self.asyncio_loop.stop()
 
-    def add_timeout(self, deadline, callback):
-        if isinstance(deadline, (int, float)):
-            delay = max(deadline - self.time(), 0)
-        elif isinstance(deadline, datetime.timedelta):
-            delay = _Timeout.timedelta_to_seconds(deadline)
-        else:
-            raise TypeError("Unsupported deadline %r", deadline)
-        return self.asyncio_loop.call_later(delay, self._run_callback,
-                                            stack_context.wrap(callback))
+    def call_at(self, when, callback, *args, **kwargs):
+        # asyncio.call_at supports *args but not **kwargs, so bind them here.
+        # We do not synchronize self.time and asyncio_loop.time, so
+        # convert from absolute to relative.
+        return self.asyncio_loop.call_later(
+            max(0, when - self.time()), self._run_callback,
+            functools.partial(stack_context.wrap(callback), *args, **kwargs))
 
     def remove_timeout(self, timeout):
         timeout.cancel()
index 18263dd943e8d86601c5a9045db707e782065054..b271dfcef17506819fb7be799b56012a8c8bf529 100644 (file)
@@ -68,6 +68,7 @@ from __future__ import absolute_import, division, print_function, with_statement
 
 import datetime
 import functools
+import numbers
 import socket
 
 import twisted.internet.abstract
@@ -90,11 +91,7 @@ from tornado.log import app_log
 from tornado.netutil import Resolver
 from tornado.stack_context import NullContext, wrap
 from tornado.ioloop import IOLoop
-
-try:
-    long  # py2
-except NameError:
-    long = int  # py3
+from tornado.util import timedelta_to_seconds
 
 
 @implementer(IDelayedCall)
@@ -475,14 +472,19 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
     def stop(self):
         self.reactor.crash()
 
-    def add_timeout(self, deadline, callback):
-        if isinstance(deadline, (int, long, float)):
+    def add_timeout(self, deadline, callback, *args, **kwargs):
+        # This method could be simplified (since tornado 4.0) by
+        # overriding call_at instead of add_timeout, but we leave it
+        # for now as a test of backwards-compatibility.
+        if isinstance(deadline, numbers.Real):
             delay = max(deadline - self.time(), 0)
         elif isinstance(deadline, datetime.timedelta):
-            delay = tornado.ioloop._Timeout.timedelta_to_seconds(deadline)
+            delay = timedelta_to_seconds(deadline)
         else:
             raise TypeError("Unsupported deadline %r")
-        return self.reactor.callLater(delay, self._run_callback, wrap(callback))
+        return self.reactor.callLater(
+            delay, self._run_callback,
+            functools.partial(wrap(callback), *args, **kwargs))
 
     def remove_timeout(self, timeout):
         if timeout.active():
index e4f07338efc1b32f35e4753ad65d6d23dd12b645..e21d5d4c5b2a99344b53e3d005fa6d2b03d3d93e 100644 (file)
@@ -155,7 +155,7 @@ class TestIOLoop(AsyncTestCase):
 
     def test_remove_timeout_after_fire(self):
         # It is not an error to call remove_timeout after it has run.
-        handle = self.io_loop.add_timeout(self.io_loop.time(), self.stop())
+        handle = self.io_loop.add_timeout(self.io_loop.time(), self.stop)
         self.wait()
         self.io_loop.remove_timeout(handle)
 
@@ -173,6 +173,18 @@ class TestIOLoop(AsyncTestCase):
         self.io_loop.add_callback(lambda: self.io_loop.add_callback(self.stop))
         self.wait()
 
+    def test_timeout_with_arguments(self):
+        # This tests that all the timeout methods pass through *args correctly.
+        results = []
+        self.io_loop.add_timeout(self.io_loop.time(), results.append, 1)
+        self.io_loop.add_timeout(datetime.timedelta(seconds=0),
+                                 results.append, 2)
+        self.io_loop.call_at(self.io_loop.time(), results.append, 3)
+        self.io_loop.call_later(0, results.append, 4)
+        self.io_loop.call_later(0, self.stop)
+        self.wait()
+        self.assertEqual(results, [1, 2, 3, 4])
+
     def test_close_file_object(self):
         """When a file object is used instead of a numeric file descriptor,
         the object should be closed (by IOLoop.close(all_fds=True),
index 49eea2c31f0e8ea5fd08e9da3aa5cc56fbd941c0..b6e06c678b7aed0d8c9f2aa6fa8022306b6bf744 100644 (file)
@@ -311,6 +311,11 @@ class ArgReplacer(object):
         return old_value, args, kwargs
 
 
+def timedelta_to_seconds(td):
+    """Equivalent to td.total_seconds() (introduced in python 2.7)."""
+    return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / float(10 ** 6)
+
+
 def _websocket_mask_python(mask, data):
     """Websocket masking function.