Skip to content

[35] [WIP] async/await fixtures #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ functions, which take funcargs, does not work. Please use
res = yield threads.deferToThread(os.listdir, tmpdir.strpath)
assert res == []


async_callbacks
=================
Using `twisted.internet.defer.ensureDeferred` as a decorator for test
functions, which take funcargs, does not work. Please use
`pytest_twisted.async_callbacks` instead::

@pytest_twisted.async_callbacks
async def test_some_stuff(tmpdir):
res = await threads.deferToThread(os.listdir, tmpdir.strpath)
assert res == []


Waiting for deferreds in fixtures
=================================
`pytest_twisted.blockon` allows fixtures to wait for deferreds::
Expand Down
48 changes: 43 additions & 5 deletions pytest_twisted.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import inspect
import sys

ASYNC_AWAIT = sys.version_info >= (3, 5)

if ASYNC_AWAIT:
import asyncio
else:
asyncio = None


import decorator
import greenlet
Expand All @@ -23,7 +32,11 @@ class _instances:


def pytest_namespace():
return {"inlineCallbacks": inlineCallbacks, "blockon": blockon}
return {
"inlineCallbacks": inlineCallbacks,
"async_callbacks": async_callbacks,
"blockon": blockon,
}


def blockon(d):
Expand Down Expand Up @@ -65,6 +78,11 @@ def inlineCallbacks(fun, *args, **kw):
return defer.inlineCallbacks(fun)(*args, **kw)


@decorator.decorator
def async_callbacks(fun, *args, **kw):
return defer.ensureDeferred(fun(*args, **kw))


def init_twisted_greenlet():
if _instances.reactor is None or _instances.gr_twisted:
return
Expand All @@ -83,28 +101,48 @@ def stop_twisted_greenlet():
_instances.gr_twisted.switch()


def is_coroutine(maybe_coroutine):
if ASYNC_AWAIT:
return asyncio.iscoroutine(maybe_coroutine)

return False


@defer.inlineCallbacks
def _pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
return testfunction(*pyfuncitem._args)
defer.returnValue(testfunction(*pyfuncitem._args))
else:
funcargs = pyfuncitem.funcargs
if hasattr(pyfuncitem, "_fixtureinfo"):
testargs = {}
for arg in pyfuncitem._fixtureinfo.argnames:
testargs[arg] = funcargs[arg]
maybe_coroutine = funcargs[arg]
if is_coroutine(maybe_coroutine):
maybe_coroutine = yield defer.ensureDeferred(
maybe_coroutine,
)
testargs[arg] = maybe_coroutine
else:
testargs = funcargs
return testfunction(**testargs)
result = yield testfunction(**testargs)
defer.returnValue(result)


def pytest_pyfunc_call(pyfuncitem):
if _instances.gr_twisted is not None:
if _instances.gr_twisted.dead:
raise RuntimeError("twisted reactor has stopped")

@defer.inlineCallbacks
def in_reactor(d, f, *args):
return defer.maybeDeferred(f, *args).chainDeferred(d)
try:
result = yield f(*args)
except Exception as e:
d.callback(failure.Failure(e))
else:
d.callback(result)

d = defer.Deferred()
_instances.reactor.callLater(
Expand Down
100 changes: 100 additions & 0 deletions testing/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

import pytest_twisted


def assert_outcomes(run_result, outcomes):
formatted_output = format_run_result_output_for_assert(run_result)
Expand Down Expand Up @@ -37,6 +39,13 @@ def skip_if_reactor_not(expected_reactor):
)


def skip_if_no_async_await():
return pytest.mark.skipif(
not pytest_twisted.ASYNC_AWAIT,
reason="async/await syntax not support on Python <3.5",
)


@pytest.fixture
def cmd_opts(request):
reactor = request.config.getoption("reactor", "default")
Expand Down Expand Up @@ -119,6 +128,44 @@ def test_succeed(foo):
rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts)
assert_outcomes(rr, {"passed": 2, "failed": 1})

def test_inlineCallbacks_exception(testdir, cmd_opts):
test_file = """
from twisted.internet import reactor, defer
import pytest
import pytest_twisted

@pytest_twisted.inlineCallbacks
def test_succeed():
yield defer.succeed(42)
raise Exception('blue')
"""
testdir.makepyfile(test_file)
rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts)
assert_outcomes(rr, {"failed": 1})



@skip_if_no_async_await()
def test_async_callbacks(testdir, cmd_opts):
test_file = """
from twisted.internet import reactor, defer
import pytest
import pytest_twisted

@pytest.fixture(scope="module", params=["fs", "imap", "web"])
def foo(request):
return request.param

@pytest_twisted.async_callbacks
async def test_succeed(foo):
await defer.succeed(foo)
if foo == "web":
raise RuntimeError("baz")
"""
testdir.makepyfile(test_file)
rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts)
assert_outcomes(rr, {"passed": 2, "failed": 1})


def test_twisted_greenlet(testdir, cmd_opts):
test_file = """
Expand Down Expand Up @@ -165,6 +212,59 @@ def test_succeed(foo):
assert_outcomes(rr, {"passed": 2, "failed": 1})


@skip_if_no_async_await()
def test_blockon_in_fixture_async(testdir, cmd_opts):
test_file = """
from twisted.internet import reactor, defer
import pytest
import pytest_twisted

@pytest.fixture(scope="module", params=["fs", "imap", "web"])
def foo(request):
d1, d2 = defer.Deferred(), defer.Deferred()
reactor.callLater(0.01, d1.callback, 1)
reactor.callLater(0.02, d2.callback, request.param)
pytest_twisted.blockon(d1)
return d2

@pytest_twisted.async_callbacks
async def test_succeed(foo):
x = await foo
if x == "web":
raise RuntimeError("baz")
"""
testdir.makepyfile(test_file)
rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts)
assert_outcomes(rr, {"passed": 2, "failed": 1})


@skip_if_no_async_await()
def test_async_fixture(testdir, cmd_opts):
test_file = """
from twisted.internet import reactor, defer
import pytest
import pytest_twisted

@pytest.fixture(scope="function", params=["fs", "imap", "web"])
async def foo(request):
d1, d2 = defer.Deferred(), defer.Deferred()
reactor.callLater(0.01, d1.callback, 1)
reactor.callLater(0.02, d2.callback, request.param)
await d1
return d2,

@pytest_twisted.inlineCallbacks
def test_succeed(foo):
x = yield foo[0]
print('+++', x)
if x == "web":
raise RuntimeError("baz")
"""
testdir.makepyfile(test_file)
rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts)
assert_outcomes(rr, {"passed": 2, "failed": 1})


@skip_if_reactor_not("default")
def test_blockon_in_hook(testdir, cmd_opts):
conftest_file = """
Expand Down