# Copyright (c) 2009-2010 Denis Bilenko. See LICENSE for details.
"""
Timeouts.
Many functions in :mod:`gevent` have a *timeout* argument that allows
limiting the time the function will block. When that is not available,
the :class:`Timeout` class and :func:`with_timeout` function in this
module add timeouts to arbitrary code.
.. warning::
    Timeouts can only work when the greenlet switches to the hub.
    If a blocking function is called or an intense calculation is ongoing during
    which no switches occur, :class:`Timeout` is powerless.
"""
from __future__ import absolute_import, print_function, division
from gevent._compat import string_types
from gevent._util import _NONE
from greenlet import getcurrent
from gevent._hub_local import get_hub_noargs as get_hub
__all__ = [
    'Timeout',
    'with_timeout',
]
class _FakeTimer(object):
    # An object that mimics the API of get_hub().loop.timer, but
    # without allocating any native resources. This is useful for timeouts
    # that will never expire.
    # Also partially mimics the API of Timeout itself for use in _start_new_or_dummy
    # This object is used as a singleton, so it should be
    # immutable.
    __slots__ = ()
    @property
    def pending(self):
        return False
    active = pending
    @property
    def seconds(self):
        "Always returns None"
    timer = exception = seconds
    def start(self, *args, **kwargs):
        # pylint:disable=unused-argument
        raise AssertionError("non-expiring timer cannot be started")
    def stop(self):
        return
    cancel = stop
    stop = close = cancel
    def __enter__(self):
        return self
    def __exit__(self, _t, _v, _tb):
        return
_FakeTimer = _FakeTimer()
[docs]
class Timeout(BaseException):
    """
    Timeout(seconds=None, exception=None, ref=True, priority=-1)
    Raise *exception* in the current greenlet after *seconds*
    have elapsed::
        timeout = Timeout(seconds, exception)
        timeout.start()
        try:
            ...  # exception will be raised here, after *seconds* passed since start() call
        finally:
            timeout.close()
    .. warning::
        You must **always** call `close` on a ``Timeout`` object you have created,
        whether or not the code that the timeout was protecting finishes
        executing before the timeout elapses (whether or not the
        ``Timeout`` exception is raised)  This ``try/finally``
        construct or a ``with`` statement is a good pattern. (If
        the timeout object will be started again, use `cancel` instead
        of `close`; this is rare. You must still `close` it when you are
        done.)
    When *exception* is omitted or ``None``, the ``Timeout`` instance
    itself is raised::
        >>> import gevent
        >>> gevent.Timeout(0.1).start()
        >>> gevent.sleep(0.2)  #doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
         ...
        Timeout: 0.1 seconds
    If the *seconds* argument is not given or is ``None`` (e.g.,
    ``Timeout()``), then the timeout will never expire and never raise
    *exception*. This is convenient for creating functions which take
    an optional timeout parameter of their own. (Note that this is **not**
    the same thing as a *seconds* value of ``0``.)
    ::
       def function(args, timeout=None):
          "A function with an optional timeout."
          timer = Timeout(timeout)
          with timer:
             ...
    .. caution::
        A *seconds* value less than ``0.0`` (e.g., ``-1``) is poorly defined. In the future,
        support for negative values is likely to do the same thing as a value
        of ``None`` or ``0``
    A *seconds* value of ``0`` requests that the event loop spin and poll for I/O;
    it will immediately expire as soon as control returns to the event loop.
    .. rubric:: Use As A Context Manager
    To simplify starting and canceling timeouts, the ``with``
    statement can be used::
        with gevent.Timeout(seconds, exception) as timeout:
            pass  # ... code block ...
    This is equivalent to the try/finally block above with one
    additional feature: if *exception* is the literal ``False``, the
    timeout is still raised, but the context manager suppresses it, so
    the code outside the with-block won't see it.
    This is handy for adding a timeout to the functions that don't
    support a *timeout* parameter themselves::
        data = None
        with gevent.Timeout(5, False):
            data = mysock.makefile().readline()
        if data is None:
            ...  # 5 seconds passed without reading a line
        else:
            ...  # a line was read within 5 seconds
    .. caution::
        If ``readline()`` above catches and doesn't re-raise
        :exc:`BaseException` (for example, with a bare ``except:``), then
        your timeout will fail to function and control won't be returned
        to you when you expect.
    .. rubric:: Catching Timeouts
    When catching timeouts, keep in mind that the one you catch may
    not be the one you have set (a calling function may have set its
    own timeout); if you going to silence a timeout, always check that
    it's the instance you need::
        timeout = Timeout(1)
        timeout.start()
        try:
            ...
        except Timeout as t:
            if t is not timeout:
                raise # not my timeout
        finally:
            timeout.close()
    .. versionchanged:: 1.1b2
        If *seconds* is not given or is ``None``, no longer allocate a
        native timer object that will never be started.
    .. versionchanged:: 1.1
        Add warning about negative *seconds* values.
    .. versionchanged:: 1.3a1
        Timeout objects now have a :meth:`close`
        method that *must* be called when the timeout will no longer be
        used to properly clean up native resources.
        The ``with`` statement does this automatically.
    .. versionchanged:: 24.10.1
          Timeout values can be compared to be less than an integer value,
          or to be less than other timeouts, e.g., ``Timeout(0) < 1`` is true.
          Timeouts are not absolutely ordered and support no other comparisons; this
          is purely for convenience and may be removed or altered in the future.
    """
    # We inherit a __dict__ from BaseException, so __slots__ actually
    # makes us larger.
    def __init__(self, seconds=None, exception=None, ref=True, priority=-1,
                 _one_shot=False):
        BaseException.__init__(self)
        self.seconds = seconds
        self.exception = exception
        self._one_shot = _one_shot
        if seconds is None:
            # Avoid going through the timer codepath if no timeout is
            # desired; this avoids some CFFI interactions on PyPy that can lead to a
            # RuntimeError if this implementation is used during an `import` statement. See
            # https://bitbucket.org/pypy/pypy/issues/2089/crash-in-pypy-260-linux64-with-gevent-11b1
            # and https://github.com/gevent/gevent/issues/618.
            # Plus, in general, it should be more efficient
            self.timer = _FakeTimer
        else:
            # XXX: A timer <= 0 could cause libuv to block the loop; we catch
            # that case in libuv/loop.py
            self.timer = get_hub().loop.timer(seconds or 0.0, ref=ref, priority=priority)
[docs]
    def start(self):
        """Schedule the timeout."""
        if self.pending:
            raise AssertionError('%r is already started; to restart it, cancel it first' % self)
        if self.seconds is None:
            # "fake" timeout (never expires)
            return
        if self.exception is None or self.exception is False or isinstance(self.exception, string_types):
            # timeout that raises self
            throws = self
        else:
            # regular timeout with user-provided exception
            throws = self.exception
        # Make sure the timer updates the current time so that we don't
        # expire prematurely.
        self.timer.start(self._on_expiration, getcurrent(), throws, update=True) 
    def _on_expiration(self, prev_greenlet, ex):
        # Hook for subclasses.
        prev_greenlet.throw(ex)
[docs]
    @classmethod
    def start_new(cls, timeout=None, exception=None, ref=True, _one_shot=False):
        """Create a started :class:`Timeout`.
        This is a shortcut, the exact action depends on *timeout*'s type:
        * If *timeout* is a :class:`Timeout`, then call its :meth:`start` method
          if it's not already begun.
        * Otherwise, create a new :class:`Timeout` instance, passing (*timeout*, *exception*) as
          arguments, then call its :meth:`start` method.
        Returns the :class:`Timeout` instance.
        """
        if isinstance(timeout, Timeout):
            if not timeout.pending:
                timeout.start()
            return timeout
        timeout = cls(timeout, exception, ref=ref, _one_shot=_one_shot)
        timeout.start()
        return timeout 
    @staticmethod
    def _start_new_or_dummy(timeout, exception=None, ref=True):
        # Internal use only in 1.1
        # Return an object with a 'cancel' method; if timeout is None,
        # this will be a shared instance object that does nothing. Otherwise,
        # return an actual Timeout. A 0 value is allowed and creates a real Timeout.
        # Because negative values are hard to reason about,
        # and are often used as sentinels in Python APIs, in the future it's likely
        # that a negative timeout will also return the shared instance.
        # This saves the previously common idiom of
        # 'timer = Timeout.start_new(t) if t is not None else None'
        # followed by 'if timer is not None: timer.cancel()'.
        # That idiom was used to avoid any object allocations.
        # A staticmethod is slightly faster under CPython, compared to a classmethod;
        # under PyPy in synthetic benchmarks it makes no difference.
        if timeout is None:
            return _FakeTimer
        return Timeout.start_new(timeout, exception, ref, _one_shot=True)
    @property
    def pending(self):
        """True if the timeout is scheduled to be raised."""
        return self.timer.pending or self.timer.active
[docs]
    def cancel(self):
        """
        If the timeout is pending, cancel it. Otherwise, do nothing.
        The timeout object can be :meth:`started <start>` again. If
        you will not start the timeout again, you should use
        :meth:`close` instead.
        """
        self.timer.stop()
        if self._one_shot:
            self.close() 
[docs]
    def close(self):
        """
        Close the timeout and free resources. The timer cannot be started again
        after this method has been used.
        """
        self.timer.stop()
        self.timer.close()
        self.timer = _FakeTimer 
    def __repr__(self):
        classname = type(self).__name__
        if self.pending:
            pending = ' pending'
        else:
            pending = ''
        if self.exception is None:
            exception = ''
        else:
            exception = ' exception=%r' % self.exception
        return '<%s at %s seconds=%s%s%s>' % (classname, hex(id(self)), self.seconds, exception, pending)
    def __str__(self):
        """
        >>> raise Timeout #doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
            ...
        Timeout
        """
        if self.seconds is None:
            return ''
        suffix = '' if self.seconds == 1 else 's'
        if self.exception is None:
            return '%s second%s' % (self.seconds, suffix)
        if self.exception is False:
            return '%s second%s (silent)' % (self.seconds, suffix)
        return '%s second%s: %s' % (self.seconds, suffix, self.exception)
[docs]
    def __enter__(self):
        """
        Start and return the timer. If the timer is already started, just return it.
        """
        if not self.pending:
            self.start()
        return self 
[docs]
    def __exit__(self, typ, value, tb):
        """
        Stop the timer.
        .. versionchanged:: 1.3a1
           The underlying native timer is also stopped. This object cannot be
           used again.
        """
        self.close()
        if value is self and self.exception is False:
            return True # Suppress the exception 
    def __lt__(self, other):
        """
        For convenience, timeouts can be compared to integers (numbers)
        based on their seconds value.
        """
        try:
            return self.seconds < other.seconds
        except AttributeError:
            try:
                return self.seconds < other
            except TypeError:
                return NotImplemented 
[docs]
def with_timeout(seconds, function, *args, **kwds):
    """Wrap a call to *function* with a timeout; if the called
    function fails to return before the timeout, cancel it and return a
    flag value, provided by *timeout_value* keyword argument.
    If timeout expires but *timeout_value* is not provided, raise :class:`Timeout`.
    Keyword argument *timeout_value* is not passed to *function*.
    """
    timeout_value = kwds.pop("timeout_value", _NONE)
    timeout = Timeout.start_new(seconds, _one_shot=True)
    try:
        try:
            return function(*args, **kwds)
        except Timeout as ex:
            if ex is timeout and timeout_value is not _NONE:
                return timeout_value
            raise
    finally:
        timeout.cancel()