#
# Copyright 2014-2016 Dustin Spicuzza
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from functools import wraps
import sys
import types
import greenlet
from tornado import gen
from tornado.stack_context import wrap as sc_wrap, NullContext
from tornado.ioloop import IOLoop
# Tornado 5.0 compatibility
try:
from tornado.concurrent import TracebackFuture as _Future
except ImportError:
from tornado.concurrent import Future as _Future
try:
from tornado.concurrent import future_set_exc_info
except ImportError:
def future_set_exc_info(future, exc_info):
future.set_exc_info(exc_info)
import logging
logger = logging.getLogger('greenado')
[docs]class TimeoutError(Exception):
"""Exception raised by ``gyield`` in timeout."""
[docs]def gcall(f, *args, **kwargs):
'''
Calls a function, makes it asynchronous, and returns the result of
the function as a :class:`tornado.concurrent.Future`. The wrapped
function may use :func:`gyield` to pseudo-synchronously wait for a
future to resolve.
This is the same code that :func:`@greenado.groutine <groutine>`
uses to wrap functions.
:param f: Function to call
:param args: Function arguments
:param kwargs: Function keyword arguments
:returns: :class:`tornado.concurrent.Future`
.. warning:: You should not discard the returned Future or exceptions
may be silently discarded, similar to a tornado coroutine.
See :func:`@gen.coroutine <tornado.gen.coroutine>` for
details.
'''
# When this function gets updated, update groutine.wrapper also!
future = _Future()
def greenlet_base():
try:
result = f(*args, **kwargs)
except Exception:
future_set_exc_info(future, sys.exc_info())
else:
future.set_result(result)
gr = greenlet.greenlet(sc_wrap(greenlet_base))
with NullContext():
gr.switch()
return future
[docs]def generator(f):
'''
A decorator that allows you to use the 'yield' keyword in a function,
without requiring callers to also yield this function.
The yield keyword can be used inside a decorated function on any
function call that returns a future object, such as functions
decorated by :func:`@gen.coroutine <tornado.gen.coroutine>`, and most
of the tornado API as of tornado 4.0.
Similar to :func:`@gen.coroutine <tornado.gen.coroutine>`, in versions
of Python before 3.3 you must raise :class:`tornado.gen.Return` to
return a value from this function.
This function must only be used by functions that either have a
:func:`@greenado.groutine <groutine>` decorator, or functions that
are children of functions that have the decorator applied.
.. versionadded:: 0.1.7
.. warning:: You should not discard the returned Future or exceptions
may be silently discarded, similar to a tornado coroutine.
See :func:`@gen.coroutine <tornado.gen.coroutine>` for
details.
'''
# Note: this code is vaguely similar to tornado's gen.coroutine/Runner,
# which is licensed under the Apache 2.0 license.
@wraps(f)
def wrapper(*args, **kwargs):
assert greenlet.getcurrent().parent is not None, "functions decorated with generator() can only be called from functions that have the @greenado.groutine decorator in the call stack."
try:
result = f(*args, **kwargs)
except (gen.Return, StopIteration) as e:
result = getattr(e, 'value', None)
else:
if isinstance(result, types.GeneratorType):
try:
future = next(result)
while True:
try:
value = gyield(future)
except Exception:
result.throw(*sys.exc_info())
else:
future = result.send(value)
except (gen.Return, StopIteration) as e:
return getattr(e, 'value', None)
return result
return wrapper
[docs]def gmoment():
'''
Similar to :func:`tornado.gen.moment`, yields the IOLoop for a single
iteration from inside a groutine.
'''
gr = greenlet.getcurrent()
assert gr.parent is not None, "gmoment() can only be called from functions that have the @greenado.groutine decorator in the call stack."
io_loop = IOLoop.current()
def _finish():
gr.switch()
io_loop.add_callback(_finish)
with NullContext():
gr.parent.switch()
[docs]def groutine(f):
'''
A decorator that makes a function asynchronous and returns the result
of the function as a :class:`tornado.concurrent.Future`. The wrapped
function may use :func:`gyield` to pseudo-synchronously wait for a
future to resolve.
The primary advantage to using this decorator is that it allows
*all* called functions and their children to use :func:`gyield`, and
doesn't require the use of generators.
If you are calling a groutine-wrapped function from a function with
a :func:`@greenado.groutine <groutine>` decorator, you will need to
use :func:`gyield` to wait for the returned future to resolve.
From a caller's perspective, this decorator is functionally
equivalent to the :func:`@gen.coroutine <tornado.gen.coroutine>`
decorator. You should not use this decorator and the
:func:`@gen.coroutine <tornado.gen.coroutine>` decorator on the same
function.
.. warning:: You should not discard the returned Future or exceptions
may be silently discarded, similar to a tornado coroutine.
See :func:`@gen.coroutine <tornado.gen.coroutine>` for
details.
'''
@wraps(f)
def wrapper(*args, **kwargs):
# When this function gets updated, update gcall also!
future = _Future()
def greenlet_base():
try:
result = f(*args, **kwargs)
except Exception:
future_set_exc_info(future, sys.exc_info())
else:
future.set_result(result)
gr = greenlet.greenlet(sc_wrap(greenlet_base))
with NullContext():
gr.switch()
return future
return wrapper
[docs]def gsleep(timeout):
'''
This will yield and allow other operations to occur in the background
before returning.
:param timeout: Number of seconds to wait
.. versionadded:: 0.1.9
'''
gr = greenlet.getcurrent()
assert gr.parent is not None, "gsleep() can only be called from functions that have the @greenado.groutine decorator in the call stack."
if timeout <= 0:
raise ValueError("Invalid timeout value '%s'" % timeout)
io_loop = IOLoop.current()
done = [False]
def on_timeout():
done[0] = True
gr.switch()
io_loop.add_timeout(io_loop.time() + timeout, on_timeout)
with NullContext():
while not done[0]:
gr.parent.switch()
[docs]def gyield(future, timeout=None):
'''
This is functionally equivalent to the 'yield' statements used in a
:func:`@gen.coroutine <tornado.gen.coroutine>`, but doesn't require
turning all your functions into generators -- so you can use the
return statement normally, and exceptions won't be accidentally
discarded.
This can be used on any function that returns a future object, such
as functions decorated by :func:`@gen.coroutine <tornado.gen.coroutine>`,
and most of the tornado API as of tornado 4.0.
This function must only be used by functions that either have a
:func:`@greenado.groutine <groutine>` decorator, or functions that are
children of functions that have the decorator applied.
:param future: A :class:`tornado.concurrent.Future` object
:param timeout: Number of seconds to wait before raising a
:exc:`TimeoutError`. Default is no timeout.
`Parameter added in version 0.1.8.`
:returns: The result set on the future object
:raises: * If an exception is set on the future, the exception
will be thrown to the caller of gyield.
* If the timeout expires, :exc:`TimeoutError` will be
raised.
.. versionchanged:: 0.1.8
Added timeout parameter
.. versionchanged:: 0.2.0
If a timeout occurs, the :exc:`TimeoutError` will not be set on the
future object, but will only be raised to the caller.
.. note: This cannot be used with :func:`tornado.gen.moment`, use
:func:`gmoment` instead
'''
gr = greenlet.getcurrent()
assert gr.parent is not None, "gyield() can only be called from functions that have the @greenado.groutine decorator in the call stack."
# don't switch/wait if the future is already ready to go
if not future.done():
io_loop = IOLoop.current()
wait_future = future
if timeout != None and timeout > 0:
# optimization: only do timeout related work if a timeout is happening..
timeout_handle = None
timeout_future = None
def on_complete(result):
if timeout_future.done():
# resolve the future so tornado doesn't complain
try:
result.result()
except Exception:
# If you don't want to see this error, then implement cancellation
# in the thing that the future came from
logger.warn("gyield() timeout expired, and this exception was ignored",
exc_info=1)
else:
timeout_future.set_result(True)
io_loop.remove_timeout(timeout_handle)
gr.switch()
def on_timeout():
timeout_future.set_exception(TimeoutError("Timeout after %s seconds" % timeout))
gr.switch()
wait_future = timeout_future = _Future()
timeout_handle = io_loop.add_timeout(
io_loop.time() + timeout,
on_timeout
)
else:
def on_complete(result):
gr.switch()
io_loop.add_future(future, on_complete)
with NullContext():
gr.parent.switch()
while not wait_future.done():
gr.parent.switch()
wait_future.result()
return future.result()