Skip to content

Commit a3e9d83

Browse files
authored
Merge pull request #45 from sashgorokhov/feature/async_fixtures
Add async fixture support (#41)
2 parents 9fb0880 + f97e5f1 commit a3e9d83

File tree

10 files changed

+180
-76
lines changed

10 files changed

+180
-76
lines changed

.travis.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
language: python
2-
python: 3.5
3-
4-
env:
5-
- TOX_ENV=py33
6-
- TOX_ENV=py34
7-
- TOX_ENV=py35
2+
python:
3+
- "3.3"
4+
- "3.4"
5+
- "3.5"
86

97
install:
10-
- pip install tox
8+
- pip install tox tox-travis
119

12-
script: tox -e $TOX_ENV
10+
script: tox
1311

1412
after_success:
1513
- pip install coveralls && cd tests && coveralls

README.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Features
4242
- fixtures for injecting unused tcp ports
4343
- pytest markers for treating tests as asyncio coroutines
4444
- easy testing with non-default event loops
45-
45+
- support of `async def` fixtures and async generator fixtures
4646

4747
Installation
4848
------------
@@ -122,6 +122,23 @@ when several unused TCP ports are required in a test.
122122
port1, port2 = unused_tcp_port_factory(), unused_tcp_port_factory()
123123
...
124124
125+
``async fixtures``
126+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
127+
This fixtures may be defined as common pytest fixture:
128+
129+
.. code-block:: python
130+
131+
@pytest.fixture(scope='function')
132+
async def async_gen_fixture():
133+
yield await asyncio.sleep(0.1)
134+
135+
@pytest.fixture(scope='function')
136+
async def async_fixture():
137+
return await asyncio.sleep(0.1)
138+
139+
They behave just like a common fixtures, except that they **must** be function-scoped.
140+
That ensures that they a run in the same event loop as test function.
141+
125142
Markers
126143
-------
127144

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

176194
0.5.0 (2016-09-07)
177195
~~~~~~~~~~~~~~~~~~

pytest_asyncio/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
"""The main point for importing pytest-asyncio items."""
22
__version__ = '0.5.0'
3-
4-
from .plugin import async_fixture

pytest_asyncio/plugin.py

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
"""pytest-asyncio implementation."""
22
import asyncio
3-
import functools
3+
import contextlib
44
import inspect
55
import socket
6-
6+
import sys
77
from concurrent.futures import ProcessPoolExecutor
8-
from contextlib import closing
98

109
import pytest
11-
12-
from _pytest.fixtures import FixtureFunctionMarker
1310
from _pytest.python import transfer_markers
1411

1512

16-
1713
class ForbiddenEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
1814
"""An event loop policy that raises errors on most operations.
1915
@@ -87,6 +83,35 @@ def pytest_fixture_setup(fixturedef, request):
8783
return outcome
8884

8985

86+
@asyncio.coroutine
87+
def initialize_async_fixtures(funcargs, testargs):
88+
"""
89+
Get async generator fixtures first value, and await coroutine fixtures
90+
"""
91+
for name, value in funcargs.items():
92+
if name not in testargs:
93+
continue
94+
if sys.version_info >= (3, 6) and inspect.isasyncgen(value):
95+
try:
96+
testargs[name] = yield from value.__anext__()
97+
except StopAsyncIteration:
98+
raise RuntimeError("async generator didn't yield") from None
99+
elif sys.version_info >= (3, 5) and inspect.iscoroutine(value):
100+
testargs[name] = yield from value
101+
102+
103+
@asyncio.coroutine
104+
def finalize_async_fixtures(funcargs, testargs):
105+
for name, value in funcargs.items():
106+
if sys.version_info >= (3, 6) and inspect.isasyncgen(value):
107+
try:
108+
yield from value.__anext__()
109+
except StopAsyncIteration:
110+
continue
111+
else:
112+
raise RuntimeError("async generator didn't stop")
113+
114+
90115
@pytest.mark.tryfirst
91116
def pytest_pyfunc_call(pyfuncitem):
92117
"""
@@ -100,8 +125,17 @@ def pytest_pyfunc_call(pyfuncitem):
100125
funcargs = pyfuncitem.funcargs
101126
testargs = {arg: funcargs[arg]
102127
for arg in pyfuncitem._fixtureinfo.argnames}
103-
event_loop.run_until_complete(
104-
asyncio.async(pyfuncitem.obj(**testargs), loop=event_loop))
128+
129+
@asyncio.coroutine
130+
def func_executor(event_loop):
131+
"""Ensure that test function and async fixtures run in one loop"""
132+
yield from initialize_async_fixtures(funcargs, testargs)
133+
try:
134+
yield from asyncio.async(pyfuncitem.obj(**testargs), loop=event_loop)
135+
finally:
136+
yield from finalize_async_fixtures(funcargs, testargs)
137+
138+
event_loop.run_until_complete(func_executor(event_loop))
105139
return True
106140

107141

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

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

162196
return port
163197
return factory
164-
165-
166-
class AsyncFixtureFunctionMarker(FixtureFunctionMarker):
167-
168-
def __init__(self, *args, **kwargs):
169-
super().__init__(*args, **kwargs)
170-
171-
def __call__(self, coroutine):
172-
"""The parameter is the actual fixture coroutine."""
173-
if not _is_coroutine(coroutine):
174-
raise ValueError('Only coroutine functions supported')
175-
176-
@functools.wraps(coroutine)
177-
def inner(*args, **kwargs):
178-
loop = None
179-
return loop.run_until_complete(coroutine(*args, **kwargs))
180-
181-
inner._pytestfixturefunction = self
182-
return inner
183-
184-
185-
def async_fixture(scope='function', params=None, autouse=False, ids=None):
186-
if callable(scope) and params is None and not autouse:
187-
# direct invocation
188-
marker = AsyncFixtureFunctionMarker(
189-
'function', params, autouse)
190-
return marker(scope)
191-
if params is not None and not isinstance(params, (list, tuple)):
192-
params = list(params)
193-
return AsyncFixtureFunctionMarker(
194-
scope, params, autouse, ids=ids)

tests/async_fixtures/__init__.py

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import asyncio
2+
import unittest.mock
3+
4+
import pytest
5+
6+
START = object()
7+
END = object()
8+
RETVAL = object()
9+
10+
11+
@pytest.fixture
12+
def mock():
13+
return unittest.mock.Mock(return_value=RETVAL)
14+
15+
16+
@pytest.fixture
17+
async def async_fixture(mock):
18+
return await asyncio.sleep(0.1, result=mock(START))
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_async_fixture(async_fixture, mock):
23+
assert mock.call_count == 1
24+
assert mock.call_args_list[-1] == unittest.mock.call(START)
25+
assert async_fixture is RETVAL
26+
27+
28+
@pytest.fixture(scope='module')
29+
async def async_fixture_module_cope():
30+
return await asyncio.sleep(0.1, result=RETVAL)
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_async_fixture_module_cope1(async_fixture_module_cope):
35+
assert async_fixture_module_cope is RETVAL
36+
37+
38+
@pytest.mark.asyncio
39+
@pytest.mark.xfail(reason='Only function scoped async fixtures are supported')
40+
async def test_async_fixture_module_cope2(async_fixture_module_cope):
41+
assert async_fixture_module_cope is RETVAL
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import asyncio
2+
import unittest.mock
3+
4+
import pytest
5+
6+
START = object()
7+
END = object()
8+
RETVAL = object()
9+
10+
11+
@pytest.fixture(scope='module')
12+
def mock():
13+
return unittest.mock.Mock(return_value=RETVAL)
14+
15+
16+
@pytest.fixture
17+
async def async_gen_fixture(mock):
18+
try:
19+
yield mock(START)
20+
except Exception as e:
21+
mock(e)
22+
else:
23+
mock(END)
24+
25+
26+
@pytest.mark.asyncio
27+
async def test_async_gen_fixture(async_gen_fixture, mock):
28+
assert mock.called
29+
assert mock.call_args_list[-1] == unittest.mock.call(START)
30+
assert async_gen_fixture is RETVAL
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_async_gen_fixture_finalized(mock):
35+
try:
36+
assert mock.called
37+
assert mock.call_args_list[-1] == unittest.mock.call(END)
38+
finally:
39+
mock.reset_mock()
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
import unittest.mock
3+
4+
import pytest
5+
6+
START = object()
7+
END = object()
8+
RETVAL = object()
9+
10+
pytestmark = pytest.mark.skip(reason='@asyncio.coroutine fixtures are not supported yet')
11+
12+
13+
@pytest.fixture
14+
def mock():
15+
return unittest.mock.Mock(return_value=RETVAL)
16+
17+
18+
@pytest.fixture
19+
@asyncio.coroutine
20+
def coroutine_fixture(mock):
21+
yield from asyncio.sleep(0.1, result=mock(START))
22+
23+
24+
@pytest.mark.asyncio
25+
@asyncio.coroutine
26+
def test_coroutine_fixture(coroutine_fixture, mock):
27+
assert mock.call_count == 1
28+
assert mock.call_args_list[-1] == unittest.mock.call(START)
29+
assert coroutine_fixture is RETVAL

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
collect_ignore.append("test_simple_35.py")
99
collect_ignore.append("markers/test_class_marker_35.py")
1010
collect_ignore.append("markers/test_module_marker_35.py")
11+
collect_ignore.append("async_fixtures/test_async_fixtures_35.py")
12+
if sys.version_info[:2] < (3, 6):
13+
collect_ignore.append("async_fixtures/test_async_gen_fixtures_36.py")
1114

1215

1316
@pytest.yield_fixture()

tests/test_async_fixtures.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)