Skip to content
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

FIX #162 "attached to a different loop", and add a new test case #164

Merged
merged 8 commits into from
Jun 2, 2020
18 changes: 11 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,28 +175,32 @@ Only test coroutines will be affected (by default, coroutines prefixed by

Changelog
---------
0.13.0 (2020-XX-XX)
~~~~~~~~~~~~~~~~~~~
- Fix `#162 <https://github.com/pytest-dev/pytest-asyncio/issues/162>`_, and ``event_loop`` fixture behavior now is coherent on all scopes.
`#164 <https://github.com/pytest-dev/pytest-asyncio/pull/164>`_

0.12.0 (2020-05-04)
~~~~~~~~~~~~~~~~~~~
- Run the event loop fixture as soon as possible. This helps with fixtures that have an implicit dependency on the event loop.
`#156` <https://github.com/pytest-dev/pytest-asyncio/pull/156>
`#156 <https://github.com/pytest-dev/pytest-asyncio/pull/156>`_

0.11.0 (2020-04-20)
~~~~~~~~~~~~~~~~~~~
- Test on 3.8, drop 3.3 and 3.4. Stick to 0.10 for these versions.
`#152` <https://github.com/pytest-dev/pytest-asyncio/pull/152>
`#152 <https://github.com/pytest-dev/pytest-asyncio/pull/152>`_
- Use the new Pytest 5.4.0 Function API. We therefore depend on pytest >= 5.4.0.
`#142` <https://github.com/pytest-dev/pytest-asyncio/pull/142>
`#142 <https://github.com/pytest-dev/pytest-asyncio/pull/142>`_
- Better ``pytest.skip`` support.
`#126` <https://github.com/pytest-dev/pytest-asyncio/pull/126>
`#126 <https://github.com/pytest-dev/pytest-asyncio/pull/126>`_

0.10.0 (2019-01-08)
~~~~~~~~~~~~~~~~~~~~
- ``pytest-asyncio`` integrates with `Hypothesis <https://hypothesis.readthedocs.io>`_
to support ``@given`` on async test functions using ``asyncio``.
`#102` <https://github.com/pytest-dev/pytest-asyncio/pull/102>
`#102 <https://github.com/pytest-dev/pytest-asyncio/pull/102>`_
- Pytest 4.1 support.
`#105` <https://github.com/pytest-dev/pytest-asyncio/pull/105>
`#105 <https://github.com/pytest-dev/pytest-asyncio/pull/105>`_

0.9.0 (2018-07-28)
~~~~~~~~~~~~~~~~~~
Expand All @@ -208,7 +212,7 @@ Changelog
0.8.0 (2017-09-23)
~~~~~~~~~~~~~~~~~~
- Improve integration with other packages (like aiohttp) with more careful event loop handling.
`#64` <https://github.com/pytest-dev/pytest-asyncio/pull/64>
`#64 <https://github.com/pytest-dev/pytest-asyncio/pull/64>`_

0.7.0 (2017-09-08)
~~~~~~~~~~~~~~~~~~
Expand Down
76 changes: 50 additions & 26 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,36 +48,61 @@ def pytest_pycollect_makeitem(collector, name, obj):
return list(collector._genfunctions(name, obj))


class FixtureStripper:
"""Include additional Fixture, and then strip them"""
REQUEST = "request"
EVENT_LOOP = "event_loop"

def __init__(self, fixturedef):
self.fixturedef = fixturedef
self.to_strip = set()

def add(self, name):
"""Add fixture name to fixturedef
and record in to_strip list (If not previously included)"""
if name in self.fixturedef.argnames:
return
self.fixturedef.argnames += (name, )
self.to_strip.add(name)

def get_and_strip_from(self, name, data_dict):
"""Strip name from data, and return value"""
result = data_dict[name]
if name in self.to_strip:
del data_dict[name]
return result

@pytest.hookimpl(trylast=True)
def pytest_fixture_post_finalizer(fixturedef, request):
"""Called after fixture teardown"""
if fixturedef.argname == "event_loop":
# Set empty loop policy, so that subsequent get_event_loop() provides a new loop
asyncio.set_event_loop_policy(None)



@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef, request):
"""Adjust the event loop policy when an event loop is produced."""
if fixturedef.argname == "event_loop" and 'asyncio' in request.keywords:
if fixturedef.argname == "event_loop":
outcome = yield
loop = outcome.get_result()
policy = asyncio.get_event_loop_policy()
try:
old_loop = policy.get_event_loop()
except RuntimeError as exc:
if 'no current event loop' not in str(exc):
raise
old_loop = None
policy.set_event_loop(loop)
fixturedef.addfinalizer(lambda: policy.set_event_loop(old_loop))
return

if isasyncgenfunction(fixturedef.func):
# This is an async generator function. Wrap it accordingly.
generator = fixturedef.func

strip_request = False
if 'request' not in fixturedef.argnames:
fixturedef.argnames += ('request', )
strip_request = True
fixture_stripper = FixtureStripper(fixturedef)
fixture_stripper.add(FixtureStripper.EVENT_LOOP)
fixture_stripper.add(FixtureStripper.REQUEST)


def wrapper(*args, **kwargs):
request = kwargs['request']
if strip_request:
del kwargs['request']
loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs)
request = fixture_stripper.get_and_strip_from(FixtureStripper.REQUEST, kwargs)

gen_obj = generator(*args, **kwargs)

Expand All @@ -96,21 +121,26 @@ async def async_finalizer():
msg = "Async generator fixture didn't stop."
msg += "Yield only once."
raise ValueError(msg)
asyncio.get_event_loop().run_until_complete(async_finalizer())
loop.run_until_complete(async_finalizer())

request.addfinalizer(finalizer)
return asyncio.get_event_loop().run_until_complete(setup())
return loop.run_until_complete(setup())

fixturedef.func = wrapper
elif inspect.iscoroutinefunction(fixturedef.func):
coro = fixturedef.func

fixture_stripper = FixtureStripper(fixturedef)
fixture_stripper.add(FixtureStripper.EVENT_LOOP)

def wrapper(*args, **kwargs):
loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs)

async def setup():
res = await coro(*args, **kwargs)
return res

return asyncio.get_event_loop().run_until_complete(setup())
return loop.run_until_complete(setup())

fixturedef.func = wrapper
yield
Expand Down Expand Up @@ -144,15 +174,9 @@ def wrap_in_sync(func, _loop):
def inner(**kwargs):
coro = func(**kwargs)
if coro is not None:
task = asyncio.ensure_future(coro, loop=_loop)
try:
loop = asyncio.get_event_loop()
except RuntimeError as exc:
if 'no current event loop' not in str(exc):
raise
loop = _loop
task = asyncio.ensure_future(coro, loop=loop)
try:
loop.run_until_complete(task)
_loop.run_until_complete(task)
except BaseException:
# run_until_complete doesn't get the result from exceptions
# that are not subclasses of `Exception`. Consume all
Expand Down
50 changes: 50 additions & 0 deletions tests/async_fixtures/test_async_fixtures_with_finalizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import functools
import pytest


@pytest.mark.asyncio
async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_event_loop_finalizer

@pytest.mark.asyncio
async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer):
await asyncio.sleep(0.01)
assert port_with_get_event_loop_finalizer

@pytest.fixture(scope="module")
def event_loop():
"""Change event_loop fixture to module level."""
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()


@pytest.fixture(scope="module")
async def port_with_event_loop_finalizer(request, event_loop):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using loop provided by event_loop fixture
# RuntimeError is raised if task is created on a different loop
await finalizer
event_loop.run_until_complete(port_afinalizer())

worker = asyncio.ensure_future(asyncio.sleep(0.2))
request.addfinalizer(functools.partial(port_finalizer, worker))
return True


@pytest.fixture(scope="module")
async def port_with_get_event_loop_finalizer(request, event_loop):
def port_finalizer(finalizer):
async def port_afinalizer():
# await task using loop provided by asyncio.get_event_loop()
# RuntimeError is raised if task is created on a different loop
await finalizer
asyncio.get_event_loop().run_until_complete(port_afinalizer())

worker = asyncio.ensure_future(asyncio.sleep(0.2))
request.addfinalizer(functools.partial(port_finalizer, worker))
return True