Skip to content

Commit 8cc38ee

Browse files
authored
Merge pull request #46 from pytest-dev/feature/async_fixtures
Feature/async fixtures
2 parents d663782 + a3e9d83 commit 8cc38ee

File tree

9 files changed

+182
-15
lines changed

9 files changed

+182
-15
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
"""The main point for importing pytest-asyncio items."""
12
__version__ = '0.5.0'

pytest_asyncio/plugin.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""pytest-asyncio implementation."""
22
import asyncio
3+
import contextlib
34
import inspect
45
import socket
5-
6+
import sys
67
from concurrent.futures import ProcessPoolExecutor
7-
from contextlib import closing
88

99
import pytest
10-
1110
from _pytest.python import transfer_markers
1211

1312

@@ -82,6 +81,35 @@ def pytest_fixture_setup(fixturedef, request):
8281
policy.set_event_loop(loop)
8382

8483

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

102139

@@ -135,7 +172,7 @@ def event_loop_process_pool(event_loop):
135172
@pytest.fixture
136173
def unused_tcp_port():
137174
"""Find an unused localhost TCP port from 1024-65535 and return it."""
138-
with closing(socket.socket()) as sock:
175+
with contextlib.closing(socket.socket()) as sock:
139176
sock.bind(('127.0.0.1', 0))
140177
return sock.getsockname()[1]
141178

@@ -146,6 +183,7 @@ def unused_tcp_port_factory():
146183
produced = set()
147184

148185
def factory():
186+
"""Return an unused port."""
149187
port = unused_tcp_port()
150188

151189
while port in produced:

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

0 commit comments

Comments
 (0)