Skip to content

Add async fixture support (#41) #45

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

Merged
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
14 changes: 6 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
language: python
python: 3.5

env:
- TOX_ENV=py33
- TOX_ENV=py34
- TOX_ENV=py35
python:
- "3.3"
- "3.4"
- "3.5"

install:
- pip install tox
- pip install tox tox-travis

script: tox -e $TOX_ENV
script: tox

after_success:
- pip install coveralls && cd tests && coveralls
20 changes: 19 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Features
- fixtures for injecting unused tcp ports
- pytest markers for treating tests as asyncio coroutines
- easy testing with non-default event loops

- support of `async def` fixtures and async generator fixtures

Installation
------------
Expand Down Expand Up @@ -122,6 +122,23 @@ when several unused TCP ports are required in a test.
port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory()
...

``async fixtures``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This fixtures may be defined as common pytest fixture:

.. code-block:: python

@pytest.fixture(scope='function')
async def async_gen_fixture():
yield await asyncio.sleep(0.1)

@pytest.fixture(scope='function')
async def async_fixture():
return await asyncio.sleep(0.1)

They behave just like a common fixtures, except that they **must** be function-scoped.
That ensures that they a run in the same event loop as test function.

Markers
-------

Expand Down Expand Up @@ -172,6 +189,7 @@ Changelog
- Using ``forbid_global_loop`` now allows tests to use ``asyncio``
subprocesses.
`#36 <https://github.com/pytest-dev/pytest-asyncio/issues/36>`_
- support for async and async gen fixtures

0.5.0 (2016-09-07)
~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 0 additions & 2 deletions pytest_asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
"""The main point for importing pytest-asyncio items."""
__version__ = '0.5.0'

from .plugin import async_fixture
83 changes: 43 additions & 40 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
"""pytest-asyncio implementation."""
import asyncio
import functools
import contextlib
import inspect
import socket

import sys
from concurrent.futures import ProcessPoolExecutor
from contextlib import closing

import pytest

from _pytest.fixtures import FixtureFunctionMarker
from _pytest.python import transfer_markers



class ForbiddenEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
"""An event loop policy that raises errors on most operations.

Expand Down Expand Up @@ -87,6 +83,35 @@ def pytest_fixture_setup(fixturedef, request):
return outcome


@asyncio.coroutine
def initialize_async_fixtures(funcargs, testargs):
"""
Get async generator fixtures first value, and await coroutine fixtures
"""
for name, value in funcargs.items():
if name not in testargs:
continue
if sys.version_info >= (3, 6) and inspect.isasyncgen(value):
try:
testargs[name] = yield from value.__anext__()
except StopAsyncIteration:
raise RuntimeError("async generator didn't yield") from None
elif sys.version_info >= (3, 5) and inspect.iscoroutine(value):
testargs[name] = yield from value


@asyncio.coroutine
def finalize_async_fixtures(funcargs, testargs):
for name, value in funcargs.items():
if sys.version_info >= (3, 6) and inspect.isasyncgen(value):
try:
yield from value.__anext__()
except StopAsyncIteration:
continue
else:
raise RuntimeError("async generator didn't stop")


@pytest.mark.tryfirst
def pytest_pyfunc_call(pyfuncitem):
"""
Expand All @@ -100,8 +125,17 @@ def pytest_pyfunc_call(pyfuncitem):
funcargs = pyfuncitem.funcargs
testargs = {arg: funcargs[arg]
for arg in pyfuncitem._fixtureinfo.argnames}
event_loop.run_until_complete(
asyncio.async(pyfuncitem.obj(**testargs), loop=event_loop))

@asyncio.coroutine
def func_executor(event_loop):
"""Ensure that test function and async fixtures run in one loop"""
yield from initialize_async_fixtures(funcargs, testargs)
try:
yield from asyncio.async(pyfuncitem.obj(**testargs), loop=event_loop)
finally:
yield from finalize_async_fixtures(funcargs, testargs)

event_loop.run_until_complete(func_executor(event_loop))
return True


Expand Down Expand Up @@ -140,7 +174,7 @@ def event_loop_process_pool(event_loop):
@pytest.fixture
def unused_tcp_port():
"""Find an unused localhost TCP port from 1024-65535 and return it."""
with closing(socket.socket()) as sock:
with contextlib.closing(socket.socket()) as sock:
sock.bind(('127.0.0.1', 0))
return sock.getsockname()[1]

Expand All @@ -161,34 +195,3 @@ def factory():

return port
return factory


class AsyncFixtureFunctionMarker(FixtureFunctionMarker):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def __call__(self, coroutine):
"""The parameter is the actual fixture coroutine."""
if not _is_coroutine(coroutine):
raise ValueError('Only coroutine functions supported')

@functools.wraps(coroutine)
def inner(*args, **kwargs):
loop = None
return loop.run_until_complete(coroutine(*args, **kwargs))

inner._pytestfixturefunction = self
return inner


def async_fixture(scope='function', params=None, autouse=False, ids=None):
if callable(scope) and params is None and not autouse:
# direct invocation
marker = AsyncFixtureFunctionMarker(
'function', params, autouse)
return marker(scope)
if params is not None and not isinstance(params, (list, tuple)):
params = list(params)
return AsyncFixtureFunctionMarker(
scope, params, autouse, ids=ids)
Empty file.
41 changes: 41 additions & 0 deletions tests/async_fixtures/test_async_fixtures_35.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import asyncio
import unittest.mock

import pytest

START = object()
END = object()
RETVAL = object()


@pytest.fixture
def mock():
return unittest.mock.Mock(return_value=RETVAL)


@pytest.fixture
async def async_fixture(mock):
return await asyncio.sleep(0.1, result=mock(START))


@pytest.mark.asyncio
async def test_async_fixture(async_fixture, mock):
assert mock.call_count == 1
assert mock.call_args_list[-1] == unittest.mock.call(START)
assert async_fixture is RETVAL


@pytest.fixture(scope='module')
async def async_fixture_module_cope():
return await asyncio.sleep(0.1, result=RETVAL)


@pytest.mark.asyncio
async def test_async_fixture_module_cope1(async_fixture_module_cope):
assert async_fixture_module_cope is RETVAL


@pytest.mark.asyncio
@pytest.mark.xfail(reason='Only function scoped async fixtures are supported')
async def test_async_fixture_module_cope2(async_fixture_module_cope):
assert async_fixture_module_cope is RETVAL
39 changes: 39 additions & 0 deletions tests/async_fixtures/test_async_gen_fixtures_36.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import asyncio
import unittest.mock

import pytest

START = object()
END = object()
RETVAL = object()


@pytest.fixture(scope='module')
def mock():
return unittest.mock.Mock(return_value=RETVAL)


@pytest.fixture
async def async_gen_fixture(mock):
try:
yield mock(START)
except Exception as e:
mock(e)
else:
mock(END)


@pytest.mark.asyncio
async def test_async_gen_fixture(async_gen_fixture, mock):
assert mock.called
assert mock.call_args_list[-1] == unittest.mock.call(START)
assert async_gen_fixture is RETVAL


@pytest.mark.asyncio
async def test_async_gen_fixture_finalized(mock):
try:
assert mock.called
assert mock.call_args_list[-1] == unittest.mock.call(END)
finally:
mock.reset_mock()
29 changes: 29 additions & 0 deletions tests/async_fixtures/test_coroutine_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import asyncio
import unittest.mock

import pytest

START = object()
END = object()
RETVAL = object()

pytestmark = pytest.mark.skip(reason='@asyncio.coroutine fixtures are not supported yet')


@pytest.fixture
def mock():
return unittest.mock.Mock(return_value=RETVAL)


@pytest.fixture
@asyncio.coroutine
def coroutine_fixture(mock):
yield from asyncio.sleep(0.1, result=mock(START))


@pytest.mark.asyncio
@asyncio.coroutine
def test_coroutine_fixture(coroutine_fixture, mock):
assert mock.call_count == 1
assert mock.call_args_list[-1] == unittest.mock.call(START)
assert coroutine_fixture is RETVAL
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
collect_ignore.append("test_simple_35.py")
collect_ignore.append("markers/test_class_marker_35.py")
collect_ignore.append("markers/test_module_marker_35.py")
collect_ignore.append("async_fixtures/test_async_fixtures_35.py")
if sys.version_info[:2] < (3, 6):
collect_ignore.append("async_fixtures/test_async_gen_fixtures_36.py")


@pytest.yield_fixture()
Expand Down
25 changes: 0 additions & 25 deletions tests/test_async_fixtures.py

This file was deleted.