Skip to content

Commit 3f5072b

Browse files
committed
Major fixtures rework.
1 parent d0df058 commit 3f5072b

14 files changed

+162
-233
lines changed

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
language: python
22
python:
3-
- "3.3"
4-
- "3.4"
53
- "3.5"
4+
- "3.6"
65

76
install:
87
- pip install tox tox-travis

README.rst

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,6 @@ provides useful fixtures and markers to make testing easier.
2222
res = await library.do_something()
2323
assert b'expected result' == res
2424
25-
or, if you're using the pre-Python 3.5 syntax:
26-
27-
.. code-block:: python
28-
29-
@pytest.mark.asyncio
30-
def test_some_asyncio_code():
31-
res = yield from library.do_something()
32-
assert b'expected result' == res
33-
3425
pytest-asyncio has been strongly influenced by pytest-tornado_.
3526

3627
.. _pytest-tornado: https://github.com/eugeniy/pytest-tornado
@@ -124,25 +115,27 @@ when several unused TCP ports are required in a test.
124115
125116
``async fixtures``
126117
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
127-
This fixtures may be defined as common pytest fixture:
118+
Asynchronous fixtures are defined similar to ordinary pytest fixtures.
128119

129120
.. code-block:: python
130-
131-
@pytest.fixture(scope='function')
121+
122+
@pytest.fixture
132123
async def async_gen_fixture():
133124
yield await asyncio.sleep(0.1)
134-
135-
@pytest.fixture(scope='function')
125+
126+
@pytest.fixture(scope='module')
136127
async def async_fixture():
137128
return await asyncio.sleep(0.1)
138129
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.
130+
All scopes are supported, but if you use a non-function scope you will need
131+
to redefine the ``event_loop`` fixture to have the same or broader scope.
132+
Async fixtures need the event loop, and so must have the same or narrower scope
133+
than the ``event_loop`` fixture.
141134

142135
Markers
143136
-------
144137

145-
``pytest.mark.asyncio(forbid_global_loop=False)``
138+
``pytest.mark.asyncio``
146139
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
147140
Mark your test coroutine with this marker and pytest will execute it as an
148141
asyncio task using the event loop provided by the ``event_loop`` fixture. See
@@ -151,9 +144,6 @@ the introductory section for an example.
151144
The event loop used can be overriden by overriding the ``event_loop`` fixture
152145
(see above).
153146

154-
If ``forbid_global_loop`` is true, ``asyncio.get_event_loop()`` will result
155-
in exceptions, ensuring your tests are always passing the event loop explicitly.
156-
157147
In order to make your test code a little more concise, the pytest |pytestmark|_
158148
feature can be used to mark entire modules or classes with this marker.
159149
Only test coroutines will be affected (by default, coroutines prefixed by
@@ -165,7 +155,7 @@ Only test coroutines will be affected (by default, coroutines prefixed by
165155
import pytest
166156
167157
# All test coroutines will be treated as marked.
168-
pytestmark = pytest.mark.asyncio(forbid_global_loop=True)
158+
pytestmark = pytest.mark.asyncio
169159
170160
async def test_example(event_loop):
171161
"""No marker!"""
@@ -174,7 +164,7 @@ Only test coroutines will be affected (by default, coroutines prefixed by
174164
.. |pytestmark| replace:: ``pytestmark``
175165
.. _pytestmark: http://doc.pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules
176166

177-
``pytest.mark.asyncio_process_pool(forbid_global_loop=False)``
167+
``pytest.mark.asyncio_process_pool``
178168
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
179169
The ``asyncio_process_pool`` marker is almost identical to the ``asyncio``
180170
marker, except the event loop used will have a
@@ -185,11 +175,11 @@ Changelog
185175

186176
0.6.0 (UNRELEASED)
187177
~~~~~~~~~~~~~~~~~~
178+
- Support for Python versions pre-3.5 has been dropped.
188179
- ``pytestmark`` now works on both module and class level.
189-
- Using ``forbid_global_loop`` now allows tests to use ``asyncio``
190-
subprocesses.
191-
`#36 <https://github.com/pytest-dev/pytest-asyncio/issues/36>`_
192-
- support for async and async gen fixtures
180+
- The ``forbid_global_loop`` parameter has been removed.
181+
- Support for async and async gen fixtures has been added.
182+
`#45 <https://github.com/pytest-dev/pytest-asyncio/pull/45>`_
193183

194184
0.5.0 (2016-09-07)
195185
~~~~~~~~~~~~~~~~~~

pytest_asyncio/plugin.py

Lines changed: 74 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,6 @@
1010
from _pytest.python import transfer_markers
1111

1212

13-
class ForbiddenEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
14-
"""An event loop policy that raises errors on most operations.
15-
16-
Operations involving child watchers are permitted."""
17-
18-
def get_event_loop(self):
19-
"""Not allowed."""
20-
raise NotImplementedError
21-
22-
def set_event_loop(self, _):
23-
"""Not allowed."""
24-
raise NotImplementedError
25-
26-
2713
def _is_coroutine(obj):
2814
"""Check to see if an object is really an asyncio coroutine."""
2915
return asyncio.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj)
@@ -62,54 +48,86 @@ def pytest_pycollect_makeitem(collector, name, obj):
6248
@pytest.hookimpl(hookwrapper=True)
6349
def pytest_fixture_setup(fixturedef, request):
6450
"""Adjust the event loop policy when an event loop is produced."""
51+
if inspect.isasyncgenfunction(fixturedef.func):
52+
# This is an async generator function. Wrap it accordingly.
53+
f = fixturedef.func
54+
55+
strip_event_loop = False
56+
if 'event_loop' not in fixturedef.argnames:
57+
fixturedef.argnames += ('event_loop', )
58+
strip_event_loop = True
59+
strip_request = False
60+
if 'request' not in fixturedef.argnames:
61+
fixturedef.argnames += ('request', )
62+
strip_request = True
63+
64+
def wrapper(*args, **kwargs):
65+
loop = kwargs['event_loop']
66+
request = kwargs['request']
67+
if strip_event_loop:
68+
del kwargs['event_loop']
69+
if strip_request:
70+
del kwargs['request']
71+
72+
gen_obj = f(*args, **kwargs)
73+
74+
async def setup():
75+
res = await gen_obj.__anext__()
76+
return res
77+
78+
def finalizer():
79+
"""Yield again, to finalize."""
80+
async def async_finalizer():
81+
try:
82+
await gen_obj.__anext__()
83+
except StopAsyncIteration:
84+
pass
85+
else:
86+
msg = "Async generator fixture didn't stop."
87+
msg += "Yield only once."
88+
raise ValueError(msg)
89+
90+
loop.run_until_complete(async_finalizer())
91+
92+
request.addfinalizer(finalizer)
93+
94+
return loop.run_until_complete(setup())
95+
96+
fixturedef.func = wrapper
97+
98+
elif inspect.iscoroutinefunction(fixturedef.func):
99+
# Just a coroutine, not an async generator.
100+
f = fixturedef.func
101+
102+
strip_event_loop = False
103+
if 'event_loop' not in fixturedef.argnames:
104+
fixturedef.argnames += ('event_loop', )
105+
strip_event_loop = True
106+
107+
def wrapper(*args, **kwargs):
108+
loop = kwargs['event_loop']
109+
if strip_event_loop:
110+
del kwargs['event_loop']
111+
112+
async def setup():
113+
res = await f(*args, **kwargs)
114+
return res
115+
116+
return loop.run_until_complete(setup())
117+
118+
fixturedef.func = wrapper
119+
65120
outcome = yield
66121

67122
if fixturedef.argname == "event_loop" and 'asyncio' in request.keywords:
68123
loop = outcome.get_result()
69124
for kw in _markers_2_fixtures.keys():
70125
if kw not in request.keywords:
71126
continue
72-
forbid_global_loop = (request.keywords[kw].kwargs
73-
.get('forbid_global_loop', False))
74-
75127
policy = asyncio.get_event_loop_policy()
76-
if forbid_global_loop:
77-
asyncio.set_event_loop_policy(ForbiddenEventLoopPolicy())
78-
asyncio.get_child_watcher().attach_loop(loop)
79-
fixturedef.addfinalizer(lambda: asyncio.set_event_loop_policy(policy))
80-
else:
81-
old_loop = policy.get_event_loop()
82-
policy.set_event_loop(loop)
83-
fixturedef.addfinalizer(lambda: policy.set_event_loop(old_loop))
84-
85-
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")
128+
old_loop = policy.get_event_loop()
129+
policy.set_event_loop(loop)
130+
fixturedef.addfinalizer(lambda: policy.set_event_loop(old_loop))
113131

114132

115133
@pytest.mark.tryfirst
@@ -126,16 +144,8 @@ def pytest_pyfunc_call(pyfuncitem):
126144
testargs = {arg: funcargs[arg]
127145
for arg in pyfuncitem._fixtureinfo.argnames}
128146

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))
147+
event_loop.run_until_complete(
148+
asyncio.async(pyfuncitem.obj(**testargs), loop=event_loop))
139149
return True
140150

141151

setup.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,14 @@ def find_version(*file_paths):
3535
"Development Status :: 4 - Beta",
3636
"Intended Audience :: Developers",
3737
"License :: OSI Approved :: Apache Software License",
38-
"Programming Language :: Python :: 3.3",
39-
"Programming Language :: Python :: 3.4",
4038
"Programming Language :: Python :: 3.5",
39+
"Programming Language :: Python :: 3.6",
4140
"Topic :: Software Development :: Testing",
4241
"Framework :: Pytest",
4342
],
4443
install_requires=[
4544
'pytest >= 3.0.6',
4645
],
47-
extras_require={
48-
':python_version == "3.3"': ['asyncio']
49-
},
5046
entry_points={
5147
'pytest11': ['asyncio = pytest_asyncio.plugin'],
5248
},

tests/async_fixtures/test_async_fixtures_35.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,3 @@ async def test_async_fixture(async_fixture, mock):
2323
assert mock.call_count == 1
2424
assert mock.call_args_list[-1] == unittest.mock.call(START)
2525
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
We support module-scoped async fixtures, but only if the event loop is
3+
module-scoped too.
4+
"""
5+
import asyncio
6+
import pytest
7+
8+
9+
@pytest.fixture(scope='module')
10+
def event_loop():
11+
"""A module-scoped event loop."""
12+
return asyncio.new_event_loop()
13+
14+
15+
@pytest.fixture(scope='module')
16+
async def async_fixture():
17+
await asyncio.sleep(0.1)
18+
return 1
19+
20+
21+
@pytest.mark.asyncio
22+
async def test_async_fixture_scope(async_fixture):
23+
assert async_fixture == 1
24+
await asyncio.sleep(0.1)

tests/async_fixtures/test_nested.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import asyncio
2+
import pytest
3+
4+
5+
@pytest.fixture()
6+
async def async_inner_fixture():
7+
await asyncio.sleep(0.01)
8+
print('inner start')
9+
yield True
10+
print('inner stop')
11+
12+
13+
@pytest.fixture()
14+
async def async_fixture_outer(async_inner_fixture, event_loop):
15+
await asyncio.sleep(0.01)
16+
print('outer start')
17+
assert async_inner_fixture is True
18+
yield True
19+
print('outer stop')
20+
21+
22+
@pytest.mark.asyncio
23+
async def test_async_fixture(async_fixture_outer):
24+
assert async_fixture_outer is True
25+
print('test_async_fixture')

tests/markers/test_class_marker.py

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

0 commit comments

Comments
 (0)