From d4c1b924c2e49a635893880cbb6745d8871a58b1 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Wed, 12 Dec 2018 19:26:08 +1100 Subject: [PATCH 1/2] Support async tests which use Hypothesis --- .gitignore | 1 + README.rst | 3 +++ pytest_asyncio/plugin.py | 32 +++++++++++++++++++++++++++- setup.py | 6 +++++- tests/test_hypothesis_integration.py | 27 +++++++++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tests/test_hypothesis_integration.py diff --git a/.gitignore b/.gitignore index a49cb172..09758085 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ var/ *.egg-info/ .installed.cfg *.egg +.hypothesis/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.rst b/README.rst index 8c36da8a..69ace41c 100644 --- a/README.rst +++ b/README.rst @@ -178,6 +178,9 @@ Changelog 0.10.0. (UNRELEASED) ~~~~~~~~~~~~~~~~~~~~ +- ``pytest-asyncio`` integrates with `Hypothesis `_ + to support ``@given`` on async test functions using ``asyncio``. + `#102` 0.9.0 (2018-07-28) ~~~~~~~~~~~~~~~~~~ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 584efbf4..88b66248 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -1,6 +1,7 @@ """pytest-asyncio implementation.""" import asyncio import contextlib +import functools import inspect import socket @@ -139,7 +140,8 @@ def pytest_pyfunc_call(pyfuncitem): function call. """ for marker_name, fixture_name in _markers_2_fixtures.items(): - if marker_name in pyfuncitem.keywords: + if isasyncgenfunction(pyfuncitem.obj) \ + and marker_name in pyfuncitem.keywords: event_loop = pyfuncitem.funcargs[fixture_name] funcargs = pyfuncitem.funcargs @@ -152,11 +154,39 @@ def pytest_pyfunc_call(pyfuncitem): return True +def wrap_in_sync(func): + """Return a sync wrapper around an async function.""" + + @functools.wraps(func) + def inner(**kwargs): + loop = asyncio.get_event_loop_policy().new_event_loop() + try: + coro = func(**kwargs) + if coro is not None: + future = asyncio.ensure_future(coro, loop=loop) + loop.run_until_complete(future) + finally: + loop.close() + + return inner + + def pytest_runtest_setup(item): for marker, fixture in _markers_2_fixtures.items(): if marker in item.keywords and fixture not in item.fixturenames: # inject an event loop fixture for all async tests item.fixturenames.append(fixture) + if item.get_closest_marker("asyncio") is not None: + if hasattr(item.obj, 'hypothesis'): + # If it's a Hypothesis test, we insert the wrap_in_sync decorator + item.obj.hypothesis.inner_test = wrap_in_sync( + item.obj.hypothesis.inner_test + ) + elif 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 + ) # maps marker to the name of the event loop fixture that will be available diff --git a/setup.py b/setup.py index 2c26c42b..97ef4603 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,11 @@ def find_version(): install_requires=["pytest >= 3.0.6"], extras_require={ ':python_version == "3.5"': "async_generator >= 1.3", - "testing": ["coverage", "async_generator >= 1.3"], + "testing": [ + "coverage", + "async_generator >= 1.3", + "hypothesis >= 3.64", + ], }, entry_points={"pytest11": ["asyncio = pytest_asyncio.plugin"]}, ) diff --git a/tests/test_hypothesis_integration.py b/tests/test_hypothesis_integration.py new file mode 100644 index 00000000..562f4772 --- /dev/null +++ b/tests/test_hypothesis_integration.py @@ -0,0 +1,27 @@ +"""Tests for the Hypothesis integration, which wraps async functions in a +sync shim for Hypothesis. +""" + +import pytest + +from hypothesis import given, strategies as st + + +@given(st.integers()) +@pytest.mark.asyncio +async def test_mark_inner(n): + assert isinstance(n, int) + + +@pytest.mark.asyncio +@given(st.integers()) +async def test_mark_outer(n): + assert isinstance(n, int) + + +@pytest.mark.parametrize("y", [1, 2]) +@given(x=st.none()) +@pytest.mark.asyncio +async def test_mark_and_parametrize(x, y): + assert x is None + assert y in (1, 2) From 1b663142480deb0acd14c9ec02b9035e4fb623ae Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Thu, 13 Dec 2018 20:25:57 +1100 Subject: [PATCH 2/2] More specific Hypothesis detection --- pytest_asyncio/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 88b66248..26a37d48 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -140,8 +140,8 @@ def pytest_pyfunc_call(pyfuncitem): function call. """ for marker_name, fixture_name in _markers_2_fixtures.items(): - if isasyncgenfunction(pyfuncitem.obj) \ - and marker_name in pyfuncitem.keywords: + if marker_name in pyfuncitem.keywords \ + and not getattr(pyfuncitem.obj, 'is_hypothesis_test', False): event_loop = pyfuncitem.funcargs[fixture_name] funcargs = pyfuncitem.funcargs