From 5d043d6fd8d6ac0722dc630a1286fde4fc8b6543 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 01:52:32 -0400 Subject: [PATCH 1/7] add support for async contextvars --- pytest_asyncio/plugin.py | 54 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 3fcea6fc..12b0aa2b 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -4,6 +4,9 @@ import functools import inspect import socket +from contextvars import Context, copy_context +from asyncio import coroutines +from asyncio.futures import Future import pytest try: @@ -65,6 +68,12 @@ def pytest_fixture_setup(fixturedef, request): fixturedef.addfinalizer(lambda: policy.set_event_loop(old_loop)) return + if 'context' in request.fixturenames: + if fixturedef.argname != 'context' and fixturedef.scope == 'function': + context = request.getfixturevalue('context') + else: + context = None + if isasyncgenfunction(fixturedef.func): # This is an async generator function. Wrap it accordingly. generator = fixturedef.func @@ -102,6 +111,7 @@ async def async_finalizer(): return asyncio.get_event_loop().run_until_complete(setup()) fixturedef.func = wrapper + elif inspect.iscoroutinefunction(fixturedef.func): coro = fixturedef.func @@ -122,6 +132,7 @@ def pytest_pyfunc_call(pyfuncitem): Run asyncio marked test functions in an event loop instead of a normal function call. """ + context = pyfuncitem.funcargs['context'] if 'asyncio' in pyfuncitem.keywords: if getattr(pyfuncitem.obj, 'is_hypothesis_test', False): pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync( @@ -154,9 +165,12 @@ def inner(**kwargs): def pytest_runtest_setup(item): - if 'asyncio' in item.keywords and 'event_loop' not in item.fixturenames: - # inject an event loop fixture for all async tests - item.fixturenames.append('event_loop') + if 'asyncio' in item.keywords: + if 'event_loop' not in item.fixturenames: + # inject an event loop fixture for all async tests + item.fixturenames.append('event_loop') + if 'context' not in item.fixturenames: + item.fixturenames.append('context') if item.get_closest_marker("asyncio") is not None \ and not getattr(item.obj, 'hypothesis', False) \ and getattr(item.obj, 'is_hypothesis_test', False): @@ -165,6 +179,40 @@ def pytest_runtest_setup(item): 'only works with Hypothesis 3.64.0 or later.' % item ) +class Task(asyncio.tasks._PyTask): + def __init__(self, coro, *, loop=None, name=None, context=None): + asyncio.futures._PyFuture.__init__(self, loop=loop) + if self._source_traceback: + del self._source_traceback[-1] + if not coroutines.iscoroutine(coro): + # raise after Future.__init__(), attrs are required for __del__ + # prevent logging for pending task in __del__ + self._log_destroy_pending = False + raise TypeError(f"a coroutine was expected, got {coro!r}") + + if name is None: + self._name = f'Task-{asyncio.tasks._task_name_counter()}' + else: + self._name = str(name) + + self._must_cancel = False + self._fut_waiter = None + self._coro = coro + self._context = context if context is not None else copy_context() + + self._loop.call_soon(self.__step, context=self._context) + asyncio._register_task(self) + + +@pytest.fixture +def context(request, event_loop): + """Create an instance of the default event loop for each test case.""" + context = Context() + def taskfactory(loop, coro): + return Task(coro, loop=event_loop, context=context) + event_loop.set_task_factory(taskfactory) + return context + @pytest.fixture def event_loop(request): From 1b1abc00d64af13d5a1d16e788de669cb4f35a94 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 01:58:38 -0400 Subject: [PATCH 2/7] remove kruft --- pytest_asyncio/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 12b0aa2b..b4eec631 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -132,7 +132,6 @@ def pytest_pyfunc_call(pyfuncitem): Run asyncio marked test functions in an event loop instead of a normal function call. """ - context = pyfuncitem.funcargs['context'] if 'asyncio' in pyfuncitem.keywords: if getattr(pyfuncitem.obj, 'is_hypothesis_test', False): pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync( From a76e5be1ceffa40385176b8e80df121d3e7de560 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 02:01:12 -0400 Subject: [PATCH 3/7] fix docstring --- pytest_asyncio/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b4eec631..70daf3f9 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -205,7 +205,7 @@ def __init__(self, coro, *, loop=None, name=None, context=None): @pytest.fixture def context(request, event_loop): - """Create an instance of the default event loop for each test case.""" + """Create an empty context for the async test case and it's async fixtures.""" context = Context() def taskfactory(loop, coro): return Task(coro, loop=event_loop, context=context) From 2a626103540cbaf899d773db1d4d5832243bed40 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 02:32:45 -0400 Subject: [PATCH 4/7] get the loop this way I guess --- pytest_asyncio/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 70daf3f9..324f4340 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -204,12 +204,12 @@ def __init__(self, coro, *, loop=None, name=None, context=None): @pytest.fixture -def context(request, event_loop): +def context(request): """Create an empty context for the async test case and it's async fixtures.""" context = Context() def taskfactory(loop, coro): - return Task(coro, loop=event_loop, context=context) - event_loop.set_task_factory(taskfactory) + return Task(coro, loop=loop, context=context) + asyncio.get_event_loop().set_task_factory(taskfactory) return context From 4c93d4d0cb944ca560e19374f368a1c434eff682 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 04:03:10 -0400 Subject: [PATCH 5/7] fix tests --- pytest_asyncio/plugin.py | 10 ++----- tests/test_contextvars.py | 21 ++++++++++++++ tests/test_hypothesis_integration.py | 42 ---------------------------- 3 files changed, 23 insertions(+), 50 deletions(-) create mode 100644 tests/test_contextvars.py delete mode 100644 tests/test_hypothesis_integration.py diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 324f4340..c6a7ccef 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -68,12 +68,6 @@ def pytest_fixture_setup(fixturedef, request): fixturedef.addfinalizer(lambda: policy.set_event_loop(old_loop)) return - if 'context' in request.fixturenames: - if fixturedef.argname != 'context' and fixturedef.scope == 'function': - context = request.getfixturevalue('context') - else: - context = None - if isasyncgenfunction(fixturedef.func): # This is an async generator function. Wrap it accordingly. generator = fixturedef.func @@ -204,12 +198,12 @@ def __init__(self, coro, *, loop=None, name=None, context=None): @pytest.fixture -def context(request): +def context(event_loop, request): """Create an empty context for the async test case and it's async fixtures.""" context = Context() def taskfactory(loop, coro): return Task(coro, loop=loop, context=context) - asyncio.get_event_loop().set_task_factory(taskfactory) + event_loop.set_task_factory(taskfactory) return context diff --git a/tests/test_contextvars.py b/tests/test_contextvars.py new file mode 100644 index 00000000..5c165e57 --- /dev/null +++ b/tests/test_contextvars.py @@ -0,0 +1,21 @@ +"""Quick'n'dirty unit tests for provided fixtures and markers.""" +import asyncio +import pytest + +import pytest_asyncio.plugin + +from contextvars import ContextVar + + +ctxvar = ContextVar('ctxvar') + + +@pytest.fixture +async def set_some_context(context): + ctxvar.set('quarantine is fun') + + +@pytest.mark.asyncio +async def test_test(set_some_context): + # print ("Context in test:", list(context.items())) + assert ctxvar.get() == 'quarantine is fun' diff --git a/tests/test_hypothesis_integration.py b/tests/test_hypothesis_integration.py deleted file mode 100644 index 9c97e06c..00000000 --- a/tests/test_hypothesis_integration.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for the Hypothesis integration, which wraps async functions in a -sync shim for Hypothesis. -""" -import asyncio - -import pytest - -from hypothesis import given, strategies as st - - -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -@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) - - -@given(st.integers()) -@pytest.mark.asyncio -async def test_can_use_fixture_provided_event_loop(event_loop, n): - semaphore = asyncio.Semaphore(value=0) - event_loop.call_soon(semaphore.release) - await semaphore.acquire() From f03f969756d4f1008933225517bdcfb18be13bef Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 10:46:57 -0400 Subject: [PATCH 6/7] restore accidentally deleted hypothesis test --- tests/test_hypothesis_integration.py | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_hypothesis_integration.py diff --git a/tests/test_hypothesis_integration.py b/tests/test_hypothesis_integration.py new file mode 100644 index 00000000..9c97e06c --- /dev/null +++ b/tests/test_hypothesis_integration.py @@ -0,0 +1,42 @@ +"""Tests for the Hypothesis integration, which wraps async functions in a +sync shim for Hypothesis. +""" +import asyncio + +import pytest + +from hypothesis import given, strategies as st + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +@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) + + +@given(st.integers()) +@pytest.mark.asyncio +async def test_can_use_fixture_provided_event_loop(event_loop, n): + semaphore = asyncio.Semaphore(value=0) + event_loop.call_soon(semaphore.release) + await semaphore.acquire() From 12fbde894cea2d9b5a893a51b7f93cbf08758af6 Mon Sep 17 00:00:00 2001 From: Jeff Laughlin Date: Sat, 18 Apr 2020 11:31:15 -0400 Subject: [PATCH 7/7] kruft --- pytest_asyncio/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index c6a7ccef..6ff3a5dc 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -6,7 +6,6 @@ import socket from contextvars import Context, copy_context from asyncio import coroutines -from asyncio.futures import Future import pytest try: