Skip to content

Commit

Permalink
Merge f9188e6 into 4e2e6bd
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Jun 28, 2020
2 parents 4e2e6bd + f9188e6 commit 3464bce
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 35 deletions.
88 changes: 53 additions & 35 deletions pytest_asyncio/plugin.py
Expand Up @@ -6,13 +6,16 @@
import socket

import pytest

try:
from _pytest.python import transfer_markers
except ImportError: # Pytest 4.1.0 removes the transfer_marker api (#104)

def transfer_markers(*args, **kwargs): # noqa
"""Noop when over pytest 4.1.0"""
pass


try:
from async_generator import isasyncgenfunction
except ImportError:
Expand All @@ -26,10 +29,12 @@ def _is_coroutine(obj):

def pytest_configure(config):
"""Inject documentation."""
config.addinivalue_line("markers",
"asyncio: "
"mark the test as a coroutine, it will be "
"run using an asyncio event loop")
config.addinivalue_line(
"markers",
"asyncio: "
"mark the test as a coroutine, it will be "
"run using an asyncio event loop",
)


@pytest.mark.tryfirst
Expand All @@ -44,12 +49,13 @@ def pytest_pycollect_makeitem(collector, name, obj):
transfer_markers(obj, item.cls, item.module)
item = pytest.Function.from_parent(collector, name=name) # To reload keywords.

if 'asyncio' in item.keywords:
if "asyncio" in item.keywords:
return list(collector._genfunctions(name, obj))


class FixtureStripper:
"""Include additional Fixture, and then strip them"""

REQUEST = "request"
EVENT_LOOP = "event_loop"

Expand All @@ -62,7 +68,7 @@ def add(self, name):
and record in to_strip list (If not previously included)"""
if name in self.fixturedef.argnames:
return
self.fixturedef.argnames += (name, )
self.fixturedef.argnames += (name,)
self.to_strip.add(name)

def get_and_strip_from(self, name, data_dict):
Expand All @@ -72,14 +78,6 @@ def get_and_strip_from(self, name, data_dict):
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):
Expand All @@ -88,7 +86,14 @@ def pytest_fixture_setup(fixturedef, request):
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):
Expand All @@ -99,10 +104,13 @@ def pytest_fixture_setup(fixturedef, request):
fixture_stripper.add(FixtureStripper.EVENT_LOOP)
fixture_stripper.add(FixtureStripper.REQUEST)


def wrapper(*args, **kwargs):
loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs)
request = fixture_stripper.get_and_strip_from(FixtureStripper.REQUEST, kwargs)
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 @@ -112,6 +120,7 @@ async def setup():

def finalizer():
"""Yield again, to finalize."""

async def async_finalizer():
try:
await gen_obj.__anext__()
Expand All @@ -121,6 +130,7 @@ async def async_finalizer():
msg = "Async generator fixture didn't stop."
msg += "Yield only once."
raise ValueError(msg)

loop.run_until_complete(async_finalizer())

request.addfinalizer(finalizer)
Expand All @@ -134,7 +144,9 @@ async def async_finalizer():
fixture_stripper.add(FixtureStripper.EVENT_LOOP)

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

async def setup():
res = await coro(*args, **kwargs)
Expand All @@ -152,16 +164,15 @@ def pytest_pyfunc_call(pyfuncitem):
Run asyncio marked test functions in an event loop instead of a normal
function call.
"""
if 'asyncio' in pyfuncitem.keywords:
if getattr(pyfuncitem.obj, 'is_hypothesis_test', False):
if "asyncio" in pyfuncitem.keywords:
if getattr(pyfuncitem.obj, "is_hypothesis_test", False):
pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
pyfuncitem.obj.hypothesis.inner_test,
_loop=pyfuncitem.funcargs['event_loop']
_loop=pyfuncitem.funcargs["event_loop"],
)
else:
pyfuncitem.obj = wrap_in_sync(
pyfuncitem.obj,
_loop=pyfuncitem.funcargs['event_loop']
pyfuncitem.obj, _loop=pyfuncitem.funcargs["event_loop"]
)
yield

Expand All @@ -184,22 +195,28 @@ def inner(**kwargs):
if task.done() and not task.cancelled():
task.exception()
raise

return inner


def pytest_runtest_setup(item):
if 'asyncio' in item.keywords:
if "asyncio" in item.keywords:
# inject an event loop fixture for all async tests
if 'event_loop' in item.fixturenames:
item.fixturenames.remove('event_loop')
item.fixturenames.insert(0, 'event_loop')
if item.get_closest_marker("asyncio") is not None \
and not getattr(item.obj, 'hypothesis', False) \
and getattr(item.obj, 'is_hypothesis_test', False):
pytest.fail(
'test function `%r` is using Hypothesis, but pytest-asyncio '
'only works with Hypothesis 3.64.0 or later.' % item
)
if "event_loop" in item.fixturenames:
item.fixturenames.remove("event_loop")
item.fixturenames.insert(0, "event_loop")
if "event_loop_policy" in item.fixturenames:
item.fixturenames.remove("event_loop_policy")
item.fixturenames.insert(0, "event_loop_policy")
if (
item.get_closest_marker("asyncio") is not None
and not getattr(item.obj, "hypothesis", False)
and getattr(item.obj, "is_hypothesis_test", False)
):
pytest.fail(
"test function `%r` is using Hypothesis, but pytest-asyncio "
"only works with Hypothesis 3.64.0 or later." % item
)


@pytest.fixture
Expand All @@ -213,7 +230,7 @@ def event_loop(request):
def _unused_tcp_port():
"""Find an unused localhost TCP port from 1024-65535 and return it."""
with contextlib.closing(socket.socket()) as sock:
sock.bind(('127.0.0.1', 0))
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]


Expand All @@ -237,4 +254,5 @@ def factory():
produced.add(port)

return port

return factory
28 changes: 28 additions & 0 deletions tests/custom_policy/conftest.py
@@ -0,0 +1,28 @@
import asyncio
from asyncio.events import BaseDefaultEventLoopPolicy

import pytest


class CustomSelectorLoop(asyncio.SelectorEventLoop):
"""A subclass with no overrides, just to test for presence."""

pass


class CustomSelectorLoopPolicy(BaseDefaultEventLoopPolicy):
def new_event_loop(self):
"""Create a new event loop.
You must call set_event_loop() to make this the current event
loop.
"""
return CustomSelectorLoop()


@pytest.fixture(autouse=True, scope="package")
def event_loop_policy():
"""Create an instance of the default event loop for each test case."""
asyncio.set_event_loop_policy(CustomSelectorLoopPolicy())
yield
asyncio.set_event_loop_policy(None)
30 changes: 30 additions & 0 deletions tests/custom_policy/test_alternative_policies.py
@@ -0,0 +1,30 @@
"""Unit tests for overriding the event loop."""
import asyncio

import pytest


@pytest.fixture
async def an_async_fixture():
"""An async fixture, to test if the loop is set correctly."""
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"
yield 1


@pytest.mark.asyncio
async def test_for_custom_loop(an_async_fixture):
"""This test should be executed using the custom loop."""
await asyncio.sleep(0.01)
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"


@pytest.mark.asyncio
async def test_dependent_fixture(dependent_fixture):
await asyncio.sleep(0.1)


@pytest.mark.asyncio
async def test_for_custom_loop_2():
"""This test should be executed using the custom loop."""
await asyncio.sleep(0.01)
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop"

0 comments on commit 3464bce

Please sign in to comment.