From 6b9e3ac3903cdb94496431785d0f8925993ee3da 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 | 49 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 0f29be12..15a2d019 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: @@ -88,6 +91,12 @@ def pytest_fixture_setup(fixturedef, request): policy.set_event_loop(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 @@ -124,6 +133,7 @@ async def async_finalizer(): return loop.run_until_complete(setup()) fixturedef.func = wrapper + elif inspect.iscoroutinefunction(fixturedef.func): coro = fixturedef.func @@ -149,6 +159,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( @@ -186,10 +197,12 @@ def inner(**kwargs): def pytest_runtest_setup(item): 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 'context' in item.fixturenames: + item.fixturenames.remove('context') + item.fixturenames.insert(0, '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): @@ -198,6 +211,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 792f3b22783b95750e7a74c705d9719de3773ca0 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 15a2d019..b7923ef7 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -159,7 +159,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 e2228635297038700c13d558746508f7b9129c5f 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 b7923ef7..3f2798cb 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -237,7 +237,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 1ebb2b4696b9210a7ff8e55375070e0b4f261338 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 3f2798cb..52632c23 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -236,12 +236,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 f7b32e4e675be5d5fe5cbd2f33f0ebf6ed1581dd 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 52632c23..3d117253 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -91,12 +91,6 @@ def pytest_fixture_setup(fixturedef, request): policy.set_event_loop(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 @@ -236,12 +230,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 7949d2243f4f13c5cd371e66aa2f357efbc9b02f 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 39dad8fa9bd6498ab8ac2b1cb440deff6d40cfaf 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 3d117253..946de267 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: