# 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):
        return 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()


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()

    .. note::

        If the code that the timeout was protecting finishes
        executing before the timeout elapses, be sure to ``close`` the
        timeout so it is not unexpectedly raised in the future. Even if it
        is raised, it is a best practice to close it. This ``try/finally``
        construct or a ``with`` statement is a recommended pattern. (If
        the timeout object will be started again, use ``cancel`` instead
        of ``close``; this is rare.)

    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.

    """

    # 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)

    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

        self.timer.start(getcurrent().throw, throws)

    @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. 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

    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()

    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)

    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

    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 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()
